This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new 9662195811e SOLR-18113: Revamp ZookeeperInfoHandler (#4124)
9662195811e is described below
commit 9662195811e54c02295f0c00c0a024e3b15a890a
Author: Eric Pugh <[email protected]>
AuthorDate: Tue Feb 17 12:39:48 2026 -0500
SOLR-18113: Revamp ZookeeperInfoHandler (#4124)
Co-authored-by: Copilot <[email protected]>
(cherry picked from commit 386cc6c89f3837c640cd34187e23a7b482dabc1c)
---
changelog/unreleased/SOLR-18113.yml | 8 +
.../solr/handler/admin/ZookeeperInfoHandler.java | 614 ++++++++++-----------
solr/core/src/resources/ImplicitPlugins.json | 2 +-
.../handler/admin/ZookeeperInfoHandlerTest.java | 195 ++++++-
solr/webapp/web/js/angular/controllers/cloud.js | 5 +-
solr/webapp/web/js/angular/services.js | 2 +-
solr/webapp/web/partials/cloud.html | 6 +-
7 files changed, 504 insertions(+), 328 deletions(-)
diff --git a/changelog/unreleased/SOLR-18113.yml
b/changelog/unreleased/SOLR-18113.yml
new file mode 100644
index 00000000000..c8f80140c01
--- /dev/null
+++ b/changelog/unreleased/SOLR-18113.yml
@@ -0,0 +1,8 @@
+# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
+title: Improve footer UI of Solr Cloud - Graph when you have large numbers of
collections. Plus code refactor.
+type: fixed # added, changed, fixed, deprecated, removed, dependency_update,
security, other
+authors:
+ - name: Eric Pugh
+links:
+ - name: SOLR-18113
+ url: https://issues.apache.org/jira/browse/SOLR-18113
diff --git
a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
index f7598af121f..f52a548a0ff 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ZookeeperInfoHandler.java
@@ -18,13 +18,8 @@ package org.apache.solr.handler.admin;
import static org.apache.solr.common.params.CommonParams.OMIT_HEADER;
import static org.apache.solr.common.params.CommonParams.PATH;
-import static org.apache.solr.common.params.CommonParams.WT;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@@ -55,23 +50,17 @@ import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.SuppressForbidden;
-import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.JSONResponseWriter;
-import org.apache.solr.response.RawResponseWriter;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
-import org.apache.zookeeper.server.ByteBufferInputStream;
-import org.noggit.CharArr;
-import org.noggit.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -117,7 +106,7 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
}
/** Enumeration of ways to filter collections on the graph panel. */
- static enum FilterType {
+ enum FilterType {
none,
name,
status
@@ -127,8 +116,8 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
static final class PageOfCollections {
List<String> selected;
int numFound = 0; // total number of matches (across all pages)
- int start = 0;
- int rows = -1;
+ int start;
+ int rows;
FilterType filterType;
String filter;
@@ -169,7 +158,7 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
if (!regexFilter.startsWith("(?i)")) regexFilter = "(?i)" + regexFilter;
Pattern filterRegex = Pattern.compile(regexFilter);
- List<String> filtered = new ArrayList<String>();
+ List<String> filtered = new ArrayList<>();
for (String next : collections) {
if (matches(filterRegex, next)) filtered.add(next);
}
@@ -182,9 +171,9 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
* user is filtering by.
*/
@SuppressWarnings("unchecked")
- final boolean matchesStatusFilter(Map<String, Object> collectionState,
Set<String> liveNodes) {
+ boolean matchesStatusFilter(Map<String, Object> collectionState,
Set<String> liveNodes) {
- if (filterType != FilterType.status || filter == null || filter.length()
== 0)
+ if (filterType != FilterType.status || filter == null ||
filter.isEmpty())
return true; // no status filter, so all match
boolean isHealthy = true; // means all replicas for all shards active
@@ -233,7 +222,7 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
return true;
}
- final boolean matches(final Pattern filter, final String collName) {
+ boolean matches(final Pattern filter, final String collName) {
return filter.matcher(collName).matches();
}
@@ -282,7 +271,7 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
throws KeeperException, InterruptedException {
if (cachedCollections == null) {
// cache is stale, rebuild the full list ...
- cachedCollections = new ArrayList<String>();
+ cachedCollections = new ArrayList<>();
List<String> fromZk = zkClient.getChildren("/collections", this);
if (fromZk != null) cachedCollections.addAll(fromZk);
@@ -304,7 +293,6 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
// activate paging (if disabled) for large collection sets
if (page.start == 0 && page.rows == -1 && page.filter == null &&
children.size() > 10) {
page.rows = 20;
- page.start = 0;
}
// apply the name filter if supplied (we don't need to pull state
@@ -337,7 +325,7 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
// using longs here as we don't know how long the 2nd group is
int leftGroup2 = Integer.parseInt(leftMatcher.group(2));
int rightGroup2 = Integer.parseInt(rightMatcher.group(2));
- return (leftGroup2 > rightGroup2) ? 1 : ((leftGroup2 ==
rightGroup2) ? 0 : -1);
+ return Integer.compare(leftGroup2, rightGroup2);
}
}
}
@@ -357,74 +345,160 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
private PagedCollectionSupport pagingSupport;
@Override
- @SuppressWarnings({"unchecked"})
public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp)
throws Exception {
final SolrParams params = req.getParams();
- Map<String, String> map = Map.of(WT, "raw", OMIT_HEADER, "true");
+
+ // Force JSON response and omit header for cleaner output
+ // Map<String, String> map = Map.of(WT, "json", OMIT_HEADER, "true");
+ Map<String, String> map = Map.of(OMIT_HEADER, "true");
req.setParams(SolrParams.wrapDefaults(new MapSolrParams(map), params));
+
+ // Ensure paging support is initialized
+ ensurePagingSupportInitialized();
+
+ // Validate parameters
+ validateParameters(params);
+
+ // Determine request type and handle accordingly
+ boolean isGraphView = "graph".equals(params.get("view"));
+ ZkBaseResponseBuilder builder =
+ isGraphView ? handleGraphViewRequest(params) :
handlePathViewRequest(params);
+
+ builder.build();
+
+ addMapToResponse(builder.getDataMap(), rsp);
+ }
+
+ /** Ensures the paging support is initialized (thread-safe lazy
initialization). */
+ private void ensurePagingSupportInitialized() {
synchronized (this) {
if (pagingSupport == null) {
pagingSupport = new PagedCollectionSupport();
ZkController zkController = cores.getZkController();
if (zkController != null) {
- // get notified when the ZK session expires (so we can clear the
cached collections and
- // rebuild)
+ // Get notified when the ZK session expires (so we can clear cached
collections)
zkController.addOnReconnectListener(pagingSupport);
}
}
}
+ }
- String path = params.get(PATH);
-
+ /**
+ * Validates request parameters to prevent illegal operations.
+ *
+ * @param params Request parameters to validate
+ */
+ private void validateParameters(SolrParams params) {
if (params.get("addr") != null) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Illegal parameter
\"addr\"");
}
+ }
+
+ /**
+ * Handles the graph view request with paginated collections.
+ *
+ * @param params Request parameters including pagination settings
+ * @return JSON string representing paginated collection data
+ */
+ private ZkBaseResponseBuilder handleGraphViewRequest(SolrParams params) {
+ // Extract pagination parameters
+ int start = params.getInt("start", 0);
+ int rows = params.getInt("rows", -1);
- String detailS = params.get(PARAM_DETAIL);
- boolean detail = detailS != null && detailS.equals("true");
+ // Extract filter parameters
+ FilterType filterType = extractFilterType(params);
+ String filter = extractFilter(params, filterType);
+
+ // Extract display options (applicable to graph view)
+ boolean detail = params.getBool(PARAM_DETAIL, false);
+ boolean dump = params.getBool("dump", false);
+
+ // Create response builder for paginated collections
+ return new ZkGraphResponseBuilder(
+ cores.getZkController(),
+ new PageOfCollections(start, rows, filterType, filter),
+ pagingSupport,
+ detail,
+ dump);
+ }
- String dumpS = params.get("dump");
- boolean dump = dumpS != null && dumpS.equals("true");
+ /**
+ * Handles the path view request for a specific ZooKeeper path.
+ *
+ * @param params Request parameters including the path to display
+ * @return JSON string representing the ZooKeeper path data
+ */
+ private ZkBaseResponseBuilder handlePathViewRequest(SolrParams params) {
+ // Extract path parameter
+ String path = params.get(PATH);
- int start = params.getInt("start", 0); // Note start ignored if rows not
specified
- int rows = params.getInt("rows", -1);
+ // Extract display options
+ boolean detail = params.getBool(PARAM_DETAIL, false);
+ boolean dump = params.getBool("dump", false);
+ // Create response builder for specific path
+ return new ZkPathResponseBuilder(cores.getZkController(), path, detail,
dump);
+ }
+
+ /**
+ * Extracts and normalizes the filter type from request parameters.
+ *
+ * @param params Request parameters
+ * @return The filter type (defaults to FilterType.none if not specified)
+ */
+ private FilterType extractFilterType(SolrParams params) {
String filterType = params.get("filterType");
if (filterType != null) {
filterType = filterType.trim().toLowerCase(Locale.ROOT);
- if (filterType.length() == 0) filterType = null;
+ if (filterType.isEmpty()) {
+ return FilterType.none;
+ }
+ return switch (filterType) {
+ case "none" -> FilterType.none;
+ case "name" -> FilterType.name;
+ case "status" -> FilterType.status;
+ default -> throw new SolrException(
+ ErrorCode.BAD_REQUEST,
+ "Invalid filterType '" + filterType + "'. Allowed values are:
none, name, status");
+ };
+ }
+ return FilterType.none;
+ }
+
+ /**
+ * Extracts and normalizes the filter value from request parameters.
+ *
+ * @param params Request parameters
+ * @param filterType The filter type being used
+ * @return The filter string, or null if not applicable
+ */
+ private String extractFilter(SolrParams params, FilterType filterType) {
+ if (filterType == FilterType.none) {
+ return null;
}
- FilterType type = (filterType != null) ? FilterType.valueOf(filterType) :
FilterType.none;
- String filter = (type != FilterType.none) ? params.get("filter") : null;
+ String filter = params.get("filter");
if (filter != null) {
filter = filter.trim();
- if (filter.length() == 0) filter = null;
+ if (!filter.isEmpty()) {
+ return filter;
+ }
}
+ return null;
+ }
- ZKPrinter printer = new ZKPrinter(cores.getZkController());
- printer.detail = detail;
- printer.dump = dump;
- boolean isGraphView = "graph".equals(params.get("view"));
- // There is no znode /clusterstate.json (removed in Solr 9), but we do as
if there's one and
- // return collection listing. Need to change services.js if cleaning up
here, collection list is
- // used from Admin UI Cloud - Graph
- boolean paginateCollections = (isGraphView &&
"/clusterstate.json".equals(path));
- printer.page = paginateCollections ? new PageOfCollections(start, rows,
type, filter) : null;
- printer.pagingSupport = pagingSupport;
-
- try {
- if (paginateCollections) {
- // List collections and allow pagination, but no specific znode info
like when looking at a
- // normal ZK path
- printer.printPaginatedCollections();
- } else {
- printer.print(path);
- }
- } finally {
- printer.close();
+ /**
+ * Adds Map data to SolrQueryResponse.
+ *
+ * @param dataMap The data map to add
+ * @param rsp The response object to populate
+ */
+ private void addMapToResponse(Map<String, Object> dataMap, SolrQueryResponse
rsp) {
+ // Add the structured data directly to the response
+ // This allows any response writer (json, xml, etc.) to serialize it
properly
+ for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
+ rsp.add(entry.getKey(), entry.getValue());
}
- rsp.getValues().add(RawResponseWriter.CONTENT, printer);
}
@SuppressForbidden(reason = "JDK String class doesn't offer a stripEnd
equivalent")
@@ -436,40 +510,59 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
//
//
--------------------------------------------------------------------------------------
- static class ZKPrinter implements ContentStream {
- static boolean FULLPATH_DEFAULT = false;
+ /**
+ * Base class for ZooKeeper response builders. Provides common functionality
for building
+ * structured response data from ZooKeeper.
+ */
+ abstract static class ZkBaseResponseBuilder {
+ protected boolean detail;
+ protected boolean dump;
- boolean indent = true;
- boolean fullpath = FULLPATH_DEFAULT;
- boolean detail = false;
- boolean dump = false;
+ protected final Map<String, Object> dataMap = new LinkedHashMap<>();
+ protected final SolrZkClient zkClient;
+ protected final ZkController zkController;
+ protected final String keeperAddr;
- String keeperAddr; // the address we're connected to
+ public ZkBaseResponseBuilder(ZkController controller, boolean detail,
boolean dump) {
+ this.zkController = controller;
+ this.detail = detail;
+ this.dump = dump;
+ this.keeperAddr = controller.getZkServerAddress();
+ this.zkClient = controller.getZkClient();
+ }
- final Utils.BAOS baos = new Utils.BAOS();
- final Writer out = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
- SolrZkClient zkClient;
+ public abstract void build() throws IOException;
- PageOfCollections page;
- PagedCollectionSupport pagingSupport;
- ZkController zkController;
+ /** Returns the data as a Map for proper serialization by response
writers. */
+ public Map<String, Object> getDataMap() {
+ return dataMap;
+ }
- public ZKPrinter(ZkController controller) throws IOException {
- this.zkController = controller;
- keeperAddr = controller.getZkServerAddress();
- zkClient = controller.getZkClient();
+ protected void writeError(int code, String msg) {
+ throw new SolrException(ErrorCode.getErrorCode(code), msg);
}
- public void close() {
- try {
- out.flush();
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ protected String time(long ms) {
+ return (new Date(ms)) + " (" + ms + ")";
+ }
+ }
+
+ /**
+ * Response builder implementation for a specific ZooKeeper path and its
data. Used by Solr Admin
+ * UI.
+ */
+ static class ZkPathResponseBuilder extends ZkBaseResponseBuilder {
+
+ private String path;
+
+ public ZkPathResponseBuilder(
+ ZkController controller, String path, boolean detail, boolean dump) {
+ super(controller, detail, dump);
+ this.path = path;
}
- // main entry point for printing from path
- void print(String path) throws IOException {
+ @Override
+ public void build() throws IOException {
if (zkClient == null) {
return;
}
@@ -479,7 +572,7 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
path = "/";
} else {
path = path.trim();
- if (path.length() == 0) {
+ if (path.isEmpty()) {
path = "/";
}
}
@@ -490,142 +583,37 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
int idx = path.lastIndexOf('/');
String parent = idx >= 0 ? path.substring(0, idx) : path;
- if (parent.length() == 0) {
+ if (parent.isEmpty()) {
parent = "/";
}
- CharArr chars = new CharArr();
- JSONWriter json = new JSONWriter(chars, 2);
- json.startObject();
-
if (detail) {
- if (!printZnode(json, path)) {
+ Map<String, Object> znodeData = buildZnodeData(path);
+ if (znodeData == null) {
return;
}
- json.writeValueSeparator();
+ dataMap.putAll(znodeData);
}
- json.writeString("tree");
- json.writeNameSeparator();
- json.startArray();
- if (!printTree(json, path)) {
+ List<Object> treeList = new ArrayList<>();
+ if (!buildTree(treeList, path)) {
return; // there was an error
}
- json.endArray();
- json.endObject();
- out.write(chars.toString());
+ dataMap.put("tree", treeList);
}
- // main entry point for printing collections
- @SuppressWarnings("unchecked")
- void printPaginatedCollections() throws IOException {
- SortedMap<String, Object> collectionStates;
- try {
- // support paging of the collections graph view (in case there are
many collections)
- // fetch the requested page of collections and then retrieve the state
for each
- pagingSupport.fetchPage(page, zkClient);
- // keep track of how many collections match the filter
- boolean applyStatusFilter = (page.filterType == FilterType.status &&
page.filter != null);
- List<String> matchesStatusFilter = applyStatusFilter ? new
ArrayList<>() : null;
- ClusterState cs = zkController.getZkStateReader().getClusterState();
- Set<String> liveNodes = applyStatusFilter ? cs.getLiveNodes() : null;
-
- collectionStates = new TreeMap<>(pagingSupport);
- for (String collection : page.selected) {
- DocCollection dc = cs.getCollectionOrNull(collection);
- if (dc != null) {
- // TODO: for collections with perReplicaState, a ser/deser to JSON
was needed to get the
- // state to render correctly for the UI?
- Map<String, Object> collectionState = dc.toMap(new
LinkedHashMap<>());
- if (applyStatusFilter) {
- // verify this collection matches the filtered state
- if (page.matchesStatusFilter(collectionState, liveNodes)) {
- matchesStatusFilter.add(collection);
- collectionStates.put(
- collection,
ClusterStatus.postProcessCollectionJSON(collectionState));
- }
- } else {
- collectionStates.put(
- collection,
ClusterStatus.postProcessCollectionJSON(collectionState));
- }
- }
- }
-
- if (applyStatusFilter) {
- // update the paged navigation info after applying the status filter
- page.selectPage(matchesStatusFilter);
-
- // rebuild the Map of state data
- SortedMap<String, Object> map = new TreeMap<String,
Object>(pagingSupport);
- for (String next : page.selected) map.put(next,
collectionStates.get(next));
- collectionStates = map;
- }
- } catch (KeeperException | InterruptedException e) {
- writeError(500, e.toString());
- return;
- }
-
- CharArr chars = new CharArr();
- JSONWriter json = new JSONWriter(chars, 2);
- json.startObject();
-
- json.writeString("znode");
- json.writeNameSeparator();
- json.startObject();
-
- // For some reason, without this the Json is badly formed
- writeKeyValue(json, PATH, "Undefined", true);
-
- if (collectionStates != null) {
- CharArr collectionOut = new CharArr();
- new JSONWriter(collectionOut, 2).write(collectionStates);
- writeKeyValue(json, "data", collectionOut.toString(), false);
- }
-
- writeKeyValue(json, "paging", page.getPagingHeader(), false);
-
- json.endObject();
- json.endObject();
- out.write(chars.toString());
- }
+ private boolean buildTree(List<Object> treeList, String path) {
+ int idx = path.lastIndexOf('/');
+ String label = idx > 0 ? path.substring(idx + 1) : path;
- void writeError(int code, String msg) throws IOException {
- throw new SolrException(ErrorCode.getErrorCode(code), msg);
- /*response.setStatus(code);
-
- CharArr chars = new CharArr();
- JSONWriter w = new JSONWriter(chars, 2);
- w.startObject();
- w.indent();
- w.writeString("status");
- w.writeNameSeparator();
- w.write(code);
- w.writeValueSeparator();
- w.indent();
- w.writeString("error");
- w.writeNameSeparator();
- w.writeString(msg);
- w.endObject();
-
- out.write(chars.toString());*/
- }
+ Map<String, Object> node = new LinkedHashMap<>();
+ node.put("text", label);
- boolean printTree(JSONWriter json, String path) throws IOException {
- String label = path;
- if (!fullpath) {
- int idx = path.lastIndexOf('/');
- label = idx > 0 ? path.substring(idx + 1) : path;
- }
- json.startObject();
- writeKeyValue(json, "text", label, true);
- json.writeValueSeparator();
- json.writeString("a_attr");
- json.writeNameSeparator();
- json.startObject();
+ Map<String, Object> aAttr = new LinkedHashMap<>();
String href =
"admin/zookeeper?detail=true&path=" + URLEncoder.encode(path,
StandardCharsets.UTF_8);
- writeKeyValue(json, "href", href, true);
- json.endObject();
+ aAttr.put("href", href);
+ node.put("a_attr", aAttr);
Stat stat = new Stat();
try {
@@ -633,86 +621,57 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
byte[] data = zkClient.getData(path, null, stat);
if (stat.getEphemeralOwner() != 0) {
- writeKeyValue(json, "ephemeral", true, false);
- writeKeyValue(json, "version", stat.getVersion(), false);
+ node.put("ephemeral", true);
+ node.put("version", stat.getVersion());
}
if (dump) {
- json.writeValueSeparator();
- printZnode(json, path);
+ Map<String, Object> znodeData = buildZnodeData(path);
+ if (znodeData != null) {
+ node.putAll(znodeData);
+ }
}
} catch (IllegalArgumentException e) {
// path doesn't exist (must have been removed)
- writeKeyValue(json, "warning", "(path gone)", false);
+ node.put("warning", "(path gone)");
} catch (KeeperException e) {
- writeKeyValue(json, "warning", e.toString(), false);
+ node.put("warning", e.toString());
log.warn("Keeper Exception", e);
} catch (InterruptedException e) {
- writeKeyValue(json, "warning", e.toString(), false);
+ node.put("warning", e.toString());
log.warn("InterruptedException", e);
}
if (stat.getNumChildren() > 0) {
- json.writeValueSeparator();
- if (indent) {
- json.indent();
- }
- json.writeString("children");
- json.writeNameSeparator();
- json.startArray();
+ List<Object> childrenList = new ArrayList<>();
try {
List<String> children = zkClient.getChildren(path, null);
java.util.Collections.sort(children);
- boolean first = true;
for (String child : children) {
- if (!first) {
- json.writeValueSeparator();
- }
-
String childPath = path + (path.endsWith("/") ? "" : "/") + child;
- if (!printTree(json, childPath)) {
+ if (!buildTree(childrenList, childPath)) {
return false;
}
- first = false;
}
- } catch (KeeperException e) {
- writeError(500, e.toString());
- return false;
- } catch (InterruptedException e) {
+ } catch (KeeperException | InterruptedException e) {
writeError(500, e.toString());
return false;
} catch (IllegalArgumentException e) {
// path doesn't exist (must have been removed)
- json.writeString("(children gone)");
+ childrenList.add("(children gone)");
}
- json.endArray();
+ node.put("children", childrenList);
}
- json.endObject();
+ treeList.add(node);
return true;
}
- String time(long ms) {
- return (new Date(ms)).toString() + " (" + ms + ")";
- }
-
- public void writeKeyValue(JSONWriter json, String k, Object v, boolean
isFirst) {
- if (!isFirst) {
- json.writeValueSeparator();
- }
- if (indent) {
- json.indent();
- }
- json.writeString(k);
- json.writeNameSeparator();
- json.write(v);
- }
-
- boolean printZnode(JSONWriter json, String path) throws IOException {
+ private Map<String, Object> buildZnodeData(String path) {
try {
String dataStr = null;
String dataStrErr = null;
@@ -723,89 +682,116 @@ public final class ZookeeperInfoHandler extends
RequestHandlerBase {
try {
dataStr = (new BytesRef(data)).utf8ToString();
} catch (Exception e) {
- dataStrErr = "data is not parsable as a utf8 String: " +
e.toString();
+ dataStrErr = "data is not parsable as a utf8 String: " + e;
}
}
- json.writeString("znode");
- json.writeNameSeparator();
- json.startObject();
-
- writeKeyValue(json, PATH, path, true);
-
- json.writeValueSeparator();
- json.writeString("prop");
- json.writeNameSeparator();
- json.startObject();
- writeKeyValue(json, "version", stat.getVersion(), true);
- writeKeyValue(json, "aversion", stat.getAversion(), false);
- writeKeyValue(json, "children_count", stat.getNumChildren(), false);
- writeKeyValue(json, "ctime", time(stat.getCtime()), false);
- writeKeyValue(json, "cversion", stat.getCversion(), false);
- writeKeyValue(json, "czxid", stat.getCzxid(), false);
- writeKeyValue(json, "ephemeralOwner", stat.getEphemeralOwner(), false);
- writeKeyValue(json, "mtime", time(stat.getMtime()), false);
- writeKeyValue(json, "mzxid", stat.getMzxid(), false);
- writeKeyValue(json, "pzxid", stat.getPzxid(), false);
- writeKeyValue(json, "dataLength", stat.getDataLength(), false);
+ SimpleOrderedMap<Object> znodeMap = new SimpleOrderedMap<>();
+ SimpleOrderedMap<Object> znodeContent = new SimpleOrderedMap<>();
+
+ znodeContent.add(PATH, path);
+
+ SimpleOrderedMap<Object> prop = new SimpleOrderedMap<>();
+ prop.add("version", stat.getVersion());
+ prop.add("aversion", stat.getAversion());
+ prop.add("children_count", stat.getNumChildren());
+ prop.add("ctime", time(stat.getCtime()));
+ prop.add("cversion", stat.getCversion());
+ prop.add("czxid", stat.getCzxid());
+ prop.add("ephemeralOwner", stat.getEphemeralOwner());
+ prop.add("mtime", time(stat.getMtime()));
+ prop.add("mzxid", stat.getMzxid());
+ prop.add("pzxid", stat.getPzxid());
+ prop.add("dataLength", stat.getDataLength());
if (null != dataStrErr) {
- writeKeyValue(json, "dataNote", dataStrErr, false);
+ prop.add("dataNote", dataStrErr);
}
- json.endObject();
+ znodeContent.add("prop", prop);
if (null != dataStr) {
- writeKeyValue(json, "data", dataStr, false);
+ znodeContent.add("data", dataStr);
}
- if (page != null) {
- writeKeyValue(json, "paging", page.getPagingHeader(), false);
- }
-
- json.endObject();
- } catch (KeeperException e) {
- writeError(500, e.toString());
- return false;
- } catch (InterruptedException e) {
+ znodeMap.add("znode", znodeContent);
+ return znodeMap;
+ } catch (KeeperException | InterruptedException e) {
writeError(500, e.toString());
- return false;
+ return null;
}
- return true;
}
+ }
- /* @Override
- public void write(OutputStream os) throws IOException {
- ByteBuffer bytes = baos.getByteBuffer();
- os.write(bytes.array(),0,bytes.limit());
- }
- */
- @Override
- public String getName() {
- return null;
- }
+ /**
+ * Response builder implementation for a paginated, filtered view of
collections. Supports
+ * pagination, and collection state retrieval.
+ */
+ static class ZkGraphResponseBuilder extends ZkBaseResponseBuilder {
+ private final PageOfCollections page;
+ private final PagedCollectionSupport pagingSupport;
- @Override
- public String getSourceInfo() {
- return null;
+ public ZkGraphResponseBuilder(
+ ZkController controller,
+ PageOfCollections page,
+ PagedCollectionSupport pagingSupport,
+ boolean detail,
+ boolean dump) {
+ super(controller, detail, dump);
+ this.page = page;
+ this.pagingSupport = pagingSupport;
}
@Override
- public String getContentType() {
- return JSONResponseWriter.CONTENT_TYPE_JSON_UTF8;
- }
+ public void build() throws IOException {
+ SortedMap<String, Object> collectionStates;
+ try {
+ // support paging of the collections graph view (in case there are
many collections)
+ // fetch the requested page of collections and then retrieve the state
for each
+ pagingSupport.fetchPage(page, zkClient);
+ // keep track of how many collections match the filter
+ boolean applyStatusFilter = (page.filterType == FilterType.status &&
page.filter != null);
+ List<String> matchesStatusFilter = applyStatusFilter ? new
ArrayList<>() : null;
+ ClusterState cs = zkController.getZkStateReader().getClusterState();
+ Set<String> liveNodes = applyStatusFilter ? cs.getLiveNodes() : null;
- @Override
- public Long getSize() {
- return null;
- }
+ collectionStates = new TreeMap<>(pagingSupport);
+ for (String collection : page.selected) {
+ DocCollection dc = cs.getCollectionOrNull(collection);
+ if (dc != null) {
+ Map<String, Object> collectionState = dc.toMap(new
LinkedHashMap<>());
+ if (applyStatusFilter) {
+ // verify this collection matches the filtered state
+ if (page.matchesStatusFilter(collectionState, liveNodes)) {
+ matchesStatusFilter.add(collection);
+ collectionStates.put(
+ collection,
ClusterStatus.postProcessCollectionJSON(collectionState));
+ }
+ } else {
+ collectionStates.put(
+ collection,
ClusterStatus.postProcessCollectionJSON(collectionState));
+ }
+ }
+ }
- @Override
- public InputStream getStream() throws IOException {
- return new ByteBufferInputStream(baos.getByteBuffer());
- }
+ if (applyStatusFilter) {
+ // update the paged navigation info after applying the status filter
+ page.selectPage(matchesStatusFilter);
- @Override
- public Reader getReader() throws IOException {
- return null;
+ // rebuild the Map of state data
+ SortedMap<String, Object> map = new TreeMap<>(pagingSupport);
+ for (String next : page.selected) map.put(next,
collectionStates.get(next));
+ collectionStates = map;
+ }
+ } catch (KeeperException | InterruptedException e) {
+ writeError(500, e.toString());
+ return;
+ }
+
+ Map<String, Object> znodeContent = new LinkedHashMap<>();
+ znodeContent.put(PATH, ZkStateReader.COLLECTIONS_ZKNODE);
+ znodeContent.put("data", collectionStates);
+ znodeContent.put("paging", page.getPagingHeader());
+
+ dataMap.put("znode", znodeContent);
}
}
}
diff --git a/solr/core/src/resources/ImplicitPlugins.json
b/solr/core/src/resources/ImplicitPlugins.json
index 83393194908..a9e8dd45ef4 100644
--- a/solr/core/src/resources/ImplicitPlugins.json
+++ b/solr/core/src/resources/ImplicitPlugins.json
@@ -158,7 +158,7 @@
]
}
},
- "queryResponseWriter": {
+ "queryResponseWriter": {
"geojson": {
"class": "solr.GeoJSONResponseWriter"
},
diff --git
a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
index f6696b7bd2c..91bbaa18180 100644
---
a/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
+++
b/solr/core/src/test/org/apache/solr/handler/admin/ZookeeperInfoHandlerTest.java
@@ -17,6 +17,7 @@
package org.apache.solr.handler.admin;
import java.io.IOException;
+import java.util.Map;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
@@ -40,7 +41,7 @@ public class ZookeeperInfoHandlerTest extends
SolrCloudTestCase {
}
@Test
- public void testZkInfoHandler() throws SolrServerException, IOException {
+ public void testZkInfoHandlerDetailView() throws SolrServerException,
IOException {
SolrClient client = cluster.getSolrClient();
ModifiableSolrParams params = new ModifiableSolrParams();
@@ -58,7 +59,7 @@ public class ZookeeperInfoHandlerTest extends
SolrCloudTestCase {
}
@Test
- public void testZkInfoHandlerCollectionsView() throws Exception {
+ public void testZkInfoHandlerGraphView() throws Exception {
// Create a test collection first
String collectionName = "zkinfo_test_collection";
CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
@@ -66,9 +67,8 @@ public class ZookeeperInfoHandlerTest extends
SolrCloudTestCase {
cluster.waitForActiveCollection(collectionName, 1, 1);
SolrClient client = cluster.getSolrClient();
- // Test collections view (graph view with clusterstate.json)
+ // Return the data to power the Solr Admin UI - Graph.
ModifiableSolrParams params = new ModifiableSolrParams();
- params.set(CommonParams.PATH, "/clusterstate.json");
params.set("view", "graph");
GenericSolrRequest req =
@@ -79,10 +79,193 @@ public class ZookeeperInfoHandlerTest extends
SolrCloudTestCase {
SimpleSolrResponse response = req.process(client);
NamedList<Object> responseData = response.getResponse();
- // Collections view should return znode with collection data
assertNotNull("Response should not be null", responseData);
-
assertNotNull(
"Response should contain 'znode' for collections view",
responseData.get("znode"));
}
+
+ @Test
+ public void testZkGraphResponseBuilderWithPagination() throws Exception {
+ // Create multiple test collections for pagination testing
+ String[] collectionNames = {
+ "zkgraph_collection_a", "zkgraph_collection_b", "zkgraph_collection_c",
"zkgraph_collection_d"
+ };
+
+ for (String collectionName : collectionNames) {
+ CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collectionName, 1, 1);
+ }
+
+ SolrClient client = cluster.getSolrClient();
+
+ // Test pagination with start=0, rows=2
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("view", "graph");
+ params.set("start", "0");
+ params.set("rows", "2");
+
+ GenericSolrRequest req =
+ new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper",
params);
+ req.setResponseParser(new JsonMapResponseParser());
+
+ SimpleSolrResponse response = req.process(client);
+ NamedList<Object> responseData = response.getResponse();
+
+ assertNotNull("Response should not be null", responseData);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> znode = (Map<String, Object>)
responseData.get("znode");
+ assertNotNull("Response should contain 'znode'", znode);
+
+ // Verify paging information is present
+ String paging = (String) znode.get("paging");
+ assertNotNull("Paging information should be present", paging);
+ assertTrue("Paging should include start position", paging.contains("0|"));
+ assertTrue("Paging should include rows", paging.contains("|2|"));
+
+ // Verify data field contains collection state (already parsed by
JsonMapResponseParser)
+ Object dataObj = znode.get("data");
+ assertNotNull("Data field should be present", dataObj);
+
+ // Data should be a Map containing collection information
+ @SuppressWarnings("unchecked")
+ Map<String, Object> collectionData = (Map<String, Object>) dataObj;
+ assertNotNull("Collection data should not be null", collectionData);
+ assertFalse("Collection data should contain collections",
collectionData.isEmpty());
+ }
+
+ @Test
+ public void testZkGraphResponseBuilderWithNameFilter() throws Exception {
+ // Create test collections with specific naming pattern
+ String[] collectionNames = {"filter_test_alpha", "filter_test_beta",
"other_collection"};
+
+ for (String collectionName : collectionNames) {
+ CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collectionName, 1, 1);
+ }
+
+ SolrClient client = cluster.getSolrClient();
+
+ // Test name filter with pattern
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("view", "graph");
+ params.set("filterType", "name");
+ params.set("filter", "filter_test*");
+
+ GenericSolrRequest req =
+ new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper",
params);
+ req.setResponseParser(new JsonMapResponseParser());
+
+ SimpleSolrResponse response = req.process(client);
+ NamedList<Object> responseData = response.getResponse();
+
+ assertNotNull("Response should not be null", responseData);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> znode = (Map<String, Object>)
responseData.get("znode");
+ assertNotNull("Response should contain 'znode'", znode);
+
+ // Verify paging information includes filter
+ String paging = (String) znode.get("paging");
+ assertNotNull("Paging information should be present", paging);
+ assertTrue("Paging should include filter type", paging.contains("name"));
+ assertTrue("Paging should include filter pattern",
paging.contains("filter_test*"));
+
+ // Verify data field contains collection state (already parsed by
JsonMapResponseParser)
+ Object dataObj = znode.get("data");
+ assertNotNull("Data field should be present", dataObj);
+
+ // Data should be a Map containing collection information
+ @SuppressWarnings("unchecked")
+ Map<String, Object> collectionData = (Map<String, Object>) dataObj;
+ assertNotNull("Collection data should not be null", collectionData);
+
+ // Verify filtered collections are present in the data
+ assertTrue(
+ "Should contain filter_test_alpha or filter_test_beta",
+ collectionData.containsKey("filter_test_alpha")
+ || collectionData.containsKey("filter_test_beta"));
+ }
+
+ @Test
+ public void testZkGraphResponseBuilderWithDetailParameter() throws Exception
{
+ // Create a test collection
+ String collectionName = "zkgraph_detail_test";
+ CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collectionName, 1, 1);
+
+ SolrClient client = cluster.getSolrClient();
+
+ // Test with detail parameter
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("view", "graph");
+ params.set("detail", "true");
+
+ GenericSolrRequest req =
+ new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper",
params);
+ req.setResponseParser(new JsonMapResponseParser());
+
+ SimpleSolrResponse response = req.process(client);
+ NamedList<Object> responseData = response.getResponse();
+
+ assertNotNull("Response should not be null", responseData);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> znode = (Map<String, Object>)
responseData.get("znode");
+ assertNotNull("Response should contain 'znode'", znode);
+
+ // Verify data field contains collection state (already parsed by
JsonMapResponseParser)
+ Object dataObj = znode.get("data");
+ assertNotNull("Data field should be present", dataObj);
+
+ // Data should be a Map containing collection information
+ @SuppressWarnings("unchecked")
+ Map<String, Object> collectionData = (Map<String, Object>) dataObj;
+ assertNotNull("Collection data should not be null", collectionData);
+ assertFalse("Collection data should contain collections",
collectionData.isEmpty());
+
+ // Verify the collection exists in the data
+ assertTrue(
+ "Data should contain the test collection",
collectionData.containsKey(collectionName));
+
+ // Verify collection has expected structure (shards, replicas, etc.)
+ @SuppressWarnings("unchecked")
+ Map<String, Object> collectionState = (Map<String, Object>)
collectionData.get(collectionName);
+ assertNotNull("Collection state should not be null", collectionState);
+ assertTrue("Collection should have shards",
collectionState.containsKey("shards"));
+ }
+
+ @Test
+ public void testZkInfoHandlerForcesJsonResponse() throws Exception {
+ // Create a test collection
+ String collectionName = "zkinfo_wt_test";
+ CollectionAdminRequest.createCollection(collectionName, "conf", 1, 1)
+ .process(cluster.getSolrClient());
+ cluster.waitForActiveCollection(collectionName, 1, 1);
+
+ SolrClient client = cluster.getSolrClient();
+
+ // Try to request XML format (wt=xml), but handler should force JSON
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set("view", "graph");
+ params.set(CommonParams.WT, "xml");
+
+ GenericSolrRequest req =
+ new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/zookeeper",
params);
+ req.setResponseParser(new JsonMapResponseParser());
+
+ // Should still get valid JSON response despite wt=xml parameter
+ SimpleSolrResponse response = req.process(client);
+ NamedList<Object> responseData = response.getResponse();
+
+ assertNotNull("Response should not be null", responseData);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> znode = (Map<String, Object>)
responseData.get("znode");
+ assertNotNull("Response should contain 'znode' (JSON format)", znode);
+
+ // Verify we got proper JSON structure with data as Map
+ Object dataObj = znode.get("data");
+ assertNotNull("Data field should be present", dataObj);
+ assertTrue("Data should be a Map (JSON was parsed), not XML string",
dataObj instanceof Map);
+ }
}
diff --git a/solr/webapp/web/js/angular/controllers/cloud.js
b/solr/webapp/web/js/angular/controllers/cloud.js
index 5bce7213b4a..e88209fda56 100644
--- a/solr/webapp/web/js/angular/controllers/cloud.js
+++ b/solr/webapp/web/js/angular/controllers/cloud.js
@@ -790,9 +790,8 @@ var graphSubController = function ($scope, Zookeeper) {
params.filter = filter;
}
- Zookeeper.clusterState(params, function (data) {
- var state = $.parseJSON(data.znode.data);
-
+ Zookeeper.clusterState(params, function (data) {
+ var state = data.znode.data;
var leaf_count = 0;
var graph_data = {
name: null,
diff --git a/solr/webapp/web/js/angular/services.js
b/solr/webapp/web/js/angular/services.js
index 6af7cc8e309..424de8d58f5 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -99,7 +99,7 @@ solrAdminServices.factory('System',
return $resource('admin/zookeeper', {wt:'json', _:Date.now()}, {
"simple": {},
"liveNodes": {params: {path: '/live_nodes'}},
- "clusterState": {params: {detail: "true", path: "/clusterstate.json"}},
+ "clusterState": {params: {detail: "true"}},
"detail": {params: {detail: "true", path: "@path"}},
"configs": {params: {detail:false, path: "/configs/"}},
"aliases": {params: {detail: "true", path: "/aliases.json"},
transformResponse:function(data) {
diff --git a/solr/webapp/web/partials/cloud.html
b/solr/webapp/web/partials/cloud.html
index d5715fbaff3..ed72cbbc90e 100644
--- a/solr/webapp/web/partials/cloud.html
+++ b/solr/webapp/web/partials/cloud.html
@@ -269,7 +269,7 @@ limitations under the License.
Filter by: <select ng-model="filterType" ng-change="initGraph()"
id="cloudGraphPagingFilterType">
<option value="status">Status</option>
<option value="name">Name</option>
- </select> T:{{filterType}}
+ </select>
<span ng-show="filterType=='status'">
<select ng-model="pagingStatusFilter"
id="cloudGraphPagingStatusFilter" ng-change="resetGraph()">
@@ -281,9 +281,9 @@ limitations under the License.
</select>
</span>
<span ng-show="filterType=='name'">
- <input ng-model="pagingFilter" type="text" size="10" name="filter"
ng-change="resetGraph()"/>
+ <input ng-model="pagingFilter" type="text" size="20" name="filter"
ng-change="resetGraph()"/>
</span>
- Show <input ng-model="rows" ng-change="resetGraph()" type="text"
size="2" name="rows" /> per page.
+ Show <input ng-model="rows" ng-change="resetGraph()"
type="text" size="2" name="rows" /> per page.
<button ng-show="nextEnabled" ng-click="next()">Next ></button>
</div>
</div>