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 <54160956+mlbis...@users.noreply.github.com>
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();

Reply via email to