This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 603d7ce7428 SOLR-17582 Stream CLUSTERSTATUS API response (#2916)
603d7ce7428 is described below
commit 603d7ce74287c1d0b44b5e304d0433784e8bd735
Author: Matthew Biscocho <[email protected]>
AuthorDate: Sat Jan 4 13:54:55 2025 -0500
SOLR-17582 Stream CLUSTERSTATUS API response (#2916)
The CLUSTERSTATUS API will now stream each collection's status to the
response, fetching and computing it on the fly. To avoid a backwards
compatibility concern, this won't work for wt=javabin.
(cherry picked from commit 1b1c92596908457a8c1c1bccaaeee82c5f122fb2)
---
solr/CHANGES.txt | 4 +-
.../apache/solr/handler/admin/ClusterStatus.java | 158 ++++++++++++---------
.../cloud/api/collections/TestCollectionAPI.java | 37 ++++-
3 files changed, 129 insertions(+), 70 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d4a65cdedb8..90178303047 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -7,7 +7,9 @@
https://github.com/apache/solr/blob/main/solr/solr-ref-guide/modules/upgrade-not
================== 9.9.0 ==================
New Features
---------------------
-(No changes)
+* SOLR-17582: The CLUSTERSTATUS API will now stream each collection's status
to the response,
+ fetching and computing it on the fly. To avoid a backwards compatibilty
concern, this won't work
+ for wt=javabin. (Matthew Biscocho, David Smiley)
Improvements
---------------------
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
index 18c8843f916..7a8ecf9c850 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
@@ -17,7 +17,6 @@
package org.apache.solr.handler.admin;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -27,15 +26,15 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
+import org.apache.solr.common.MapWriter;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
-import org.apache.solr.common.cloud.DocRouter;
import org.apache.solr.common.cloud.PerReplicaStates;
import org.apache.solr.common.cloud.Replica;
-import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ShardParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
@@ -180,6 +179,8 @@ public class ClusterStatus {
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();
@@ -205,54 +206,35 @@ public class ClusterStatus {
}
}
- // TODO use an Iterable to stream the data to the client instead of
gathering it all in mem
-
- NamedList<Object> collectionProps = new SimpleOrderedMap<>();
-
- collectionStream.forEach(
- clusterStateCollection -> {
- Map<String, Object> collectionStatus;
- String name = clusterStateCollection.getName();
-
- Set<String> requestedShards = new HashSet<>();
- if (routeKey != null) {
- DocRouter router = clusterStateCollection.getRouter();
- Collection<Slice> slices =
- router.getSearchSlices(routeKey, null, clusterStateCollection);
- for (Slice slice : slices) {
- requestedShards.add(slice.getName());
- }
- }
- if (shard != null) {
- String[] paramShards = shard.split(",");
- requestedShards.addAll(Arrays.asList(paramShards));
- }
-
- byte[] bytes = Utils.toJSON(clusterStateCollection);
- @SuppressWarnings("unchecked")
- Map<String, Object> docCollection = (Map<String, Object>)
Utils.fromJSON(bytes);
- collectionStatus = getCollectionStatus(docCollection, name,
requestedShards);
-
- collectionStatus.put("znodeVersion",
clusterStateCollection.getZNodeVersion());
- collectionStatus.put(
- "creationTimeMillis",
clusterStateCollection.getCreationTime().toEpochMilli());
-
- if (collectionVsAliases.containsKey(name) &&
!collectionVsAliases.get(name).isEmpty()) {
- collectionStatus.put("aliases", collectionVsAliases.get(name));
- }
- String configName = clusterStateCollection.getConfigName();
- collectionStatus.put("configName", configName);
- if (solrParams.getBool("prs", false) &&
clusterStateCollection.isPerReplicaState()) {
- PerReplicaStates prs =
clusterStateCollection.getPerReplicaStates();
- collectionStatus.put("PRS", prs);
- }
- collectionProps.add(name, collectionStatus);
- });
-
- // now we need to walk the collectionProps tree to cross-check replica
state with live nodes
- crossCheckReplicaStateWithLiveNodes(liveNodes, collectionProps);
-
- clusterStatus.add("collections", collectionProps);
+ // Because of back-compat for SolrJ, create the whole response into a
NamedList
+ // Otherwise stream with MapWriter to save memory
+ if (CommonParams.JAVABIN.equals(solrParams.get(CommonParams.WT))) {
+ NamedList<Object> collectionProps = new SimpleOrderedMap<>();
+ collectionStream.forEach(
+ collectionState -> {
+ collectionProps.add(
+ collectionState.getName(),
+ buildResponseForCollection(
+ collectionState, collectionVsAliases, routeKey, liveNodes,
requestedShards));
+ });
+ clusterStatus.add("collections", collectionProps);
+ } else {
+ MapWriter collectionPropsWriter =
+ ew -> {
+ collectionStream.forEach(
+ (collectionState) -> {
+ ew.putNoEx(
+ collectionState.getName(),
+ buildResponseForCollection(
+ collectionState,
+ collectionVsAliases,
+ routeKey,
+ liveNodes,
+ requestedShards));
+ });
+ };
+ clusterStatus.add("collections", collectionPropsWriter);
+ }
}
private void addAliasMap(Aliases aliases, NamedList<Object> clusterStatus) {
@@ -307,23 +289,20 @@ public class ClusterStatus {
*/
@SuppressWarnings("unchecked")
protected void crossCheckReplicaStateWithLiveNodes(
- List<String> liveNodes, NamedList<Object> collectionProps) {
- for (Map.Entry<String, Object> next : collectionProps) {
- Map<String, Object> collMap = (Map<String, Object>) next.getValue();
- Map<String, Object> shards = (Map<String, Object>) collMap.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;
- if (Replica.State.getState((String)
replicaMap.get(ZkStateReader.STATE_PROP))
- != Replica.State.DOWN) {
- // not down, so verify the node is live
- String node_name = (String)
replicaMap.get(ZkStateReader.NODE_NAME_PROP);
- if (!liveNodes.contains(node_name)) {
- // node is not live, so this replica is actually down
- replicaMap.put(ZkStateReader.STATE_PROP,
Replica.State.DOWN.toString());
- }
+ List<String> liveNodes, Map<String, Object> collectionProps) {
+ var shards = (Map<String, Object>) collectionProps.get("shards");
+ for (Object nextShard : shards.values()) {
+ var shardMap = (Map<String, Object>) nextShard;
+ var replicas = (Map<String, Object>) shardMap.get("replicas");
+ for (Object nextReplica : replicas.values()) {
+ var replicaMap = (Map<String, Object>) nextReplica;
+ if (Replica.State.getState((String)
replicaMap.get(ZkStateReader.STATE_PROP))
+ != Replica.State.DOWN) {
+ // not down, so verify the node is live
+ String node_name = (String)
replicaMap.get(ZkStateReader.NODE_NAME_PROP);
+ if (!liveNodes.contains(node_name)) {
+ // node is not live, so this replica is actually down
+ replicaMap.put(ZkStateReader.STATE_PROP,
Replica.State.DOWN.toString());
}
}
}
@@ -368,4 +347,47 @@ public class ClusterStatus {
collection.put("health", Health.combine(healthStates).toString());
return collection;
}
+
+ private Map<String, Object> buildResponseForCollection(
+ DocCollection clusterStateCollection,
+ Map<String, List<String>> collectionVsAliases,
+ String routeKey,
+ List<String> liveNodes,
+ Set<String> requestedShards) {
+ Map<String, Object> collectionStatus;
+ Set<String> shards = new HashSet<>();
+ String name = clusterStateCollection.getName();
+
+ if (routeKey != null)
+ clusterStateCollection
+ .getRouter()
+ .getSearchSlices(routeKey, null, clusterStateCollection)
+ .forEach((slice) -> shards.add(slice.getName()));
+
+ if (requestedShards != null) shards.addAll(requestedShards);
+
+ byte[] bytes = Utils.toJSON(clusterStateCollection);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> docCollection = (Map<String, Object>)
Utils.fromJSON(bytes);
+ collectionStatus = getCollectionStatus(docCollection, name, shards);
+
+ collectionStatus.put("znodeVersion",
clusterStateCollection.getZNodeVersion());
+ collectionStatus.put(
+ "creationTimeMillis",
clusterStateCollection.getCreationTime().toEpochMilli());
+
+ if (collectionVsAliases.containsKey(name) &&
!collectionVsAliases.get(name).isEmpty()) {
+ collectionStatus.put("aliases", collectionVsAliases.get(name));
+ }
+ String configName = clusterStateCollection.getConfigName();
+ collectionStatus.put("configName", configName);
+ if (solrParams.getBool("prs", false) &&
clusterStateCollection.isPerReplicaState()) {
+ PerReplicaStates prs = clusterStateCollection.getPerReplicaStates();
+ collectionStatus.put("PRS", prs);
+ }
+
+ // now we need to walk the collectionProps tree to cross-check replica
state with live nodes
+ crossCheckReplicaStateWithLiveNodes(liveNodes, collectionStatus);
+
+ return collectionStatus;
+ }
}
diff --git
a/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
b/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
index b3e7cfb6d92..c9e1d84a468 100644
---
a/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
+++
b/solr/core/src/test/org/apache/solr/cloud/api/collections/TestCollectionAPI.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.cloud.api.collections;
+import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
@@ -31,6 +32,7 @@ import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.BaseHttpSolrClient;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.NoOpResponseParser;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.request.V2Request;
@@ -85,7 +87,6 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
client.request(req);
createCollection(null, COLLECTION_NAME1, 1, 1, client, null, "conf1");
}
-
waitForCollection(ZkStateReader.from(cloudClient), COLLECTION_NAME, 2);
waitForCollection(ZkStateReader.from(cloudClient), COLLECTION_NAME1, 1);
waitForRecoveriesToFinish(COLLECTION_NAME, false);
@@ -95,6 +96,7 @@ public class TestCollectionAPI extends ReplicaPropertiesBase {
clusterStatusNoCollection();
clusterStatusWithCollection();
clusterStatusWithCollectionAndShard();
+ clusterStatusWithCollectionAndShardJSON();
clusterStatusWithCollectionAndMultipleShards();
clusterStatusWithCollectionHealthState();
clusterStatusWithRouteKey();
@@ -669,6 +671,39 @@ public class TestCollectionAPI extends
ReplicaPropertiesBase {
}
}
+ @SuppressWarnings("unchecked")
+ private void clusterStatusWithCollectionAndShardJSON() throws IOException,
SolrServerException {
+
+ try (CloudSolrClient client = createCloudClient(null)) {
+ ObjectMapper mapper = new ObjectMapper();
+
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("action",
CollectionParams.CollectionAction.CLUSTERSTATUS.toString());
+ params.set("collection", COLLECTION_NAME);
+ params.set("shard", SHARD1);
+ params.set("wt", "json");
+ QueryRequest request = new QueryRequest(params);
+ request.setResponseParser(new NoOpResponseParser("json"));
+ request.setPath("/admin/collections");
+ NamedList<Object> rsp = client.request(request);
+ String actualResponse = (String) rsp.get("response");
+
+ Map<String, Object> result = mapper.readValue(actualResponse, Map.class);
+
+ var cluster = (Map<String, Object>) result.get("cluster");
+ assertNotNull("Cluster state should not be null", cluster);
+ var collections = (Map<String, Object>) cluster.get("collections");
+ assertNotNull("Collections should not be null in cluster state",
collections);
+ assertNotNull(collections.get(COLLECTION_NAME));
+ assertEquals(1, collections.size());
+ var collection = (Map<String, Object>) collections.get(COLLECTION_NAME);
+ var shardStatus = (Map<String, Object>) collection.get("shards");
+ assertEquals(1, shardStatus.size());
+ Map<String, Object> selectedShardStatus = (Map<String, Object>)
shardStatus.get(SHARD1);
+ assertNotNull(selectedShardStatus);
+ }
+ }
+
private void clusterStatusRolesTest() throws Exception {
try (CloudSolrClient client = createCloudClient(null)) {
client.connect();