You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by th...@apache.org on 2014/12/31 00:16:12 UTC
svn commit: r1648630 - in /lucene/dev/branches/branch_5x: ./ solr/
solr/core/ solr/core/src/java/org/apache/solr/servlet/ solr/webapp/
solr/webapp/web/css/styles/ solr/webapp/web/js/scripts/ solr/webapp/web/tpl/
Author: thelabdude
Date: Tue Dec 30 23:16:11 2014
New Revision: 1648630
URL: http://svn.apache.org/r1648630
Log:
SOLR-5810: backport to branch 5x
Modified:
lucene/dev/branches/branch_5x/ (props changed)
lucene/dev/branches/branch_5x/solr/ (props changed)
lucene/dev/branches/branch_5x/solr/core/ (props changed)
lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java
lucene/dev/branches/branch_5x/solr/webapp/ (props changed)
lucene/dev/branches/branch_5x/solr/webapp/web/css/styles/cloud.css
lucene/dev/branches/branch_5x/solr/webapp/web/js/scripts/cloud.js
lucene/dev/branches/branch_5x/solr/webapp/web/tpl/cloud.html
Modified: lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java?rev=1648630&r1=1648629&r2=1648630&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java (original)
+++ lucene/dev/branches/branch_5x/solr/core/src/java/org/apache/solr/servlet/ZookeeperInfoServlet.java Tue Dec 30 23:16:11 2014
@@ -22,26 +22,36 @@ import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.Date;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.lucene.util.BytesRef;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.cloud.OnReconnect;
import org.apache.solr.common.cloud.SolrZkClient;
-import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.util.FastWriter;
import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.WatchedEvent;
+import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.noggit.CharArr;
import org.noggit.JSONWriter;
@@ -56,12 +66,259 @@ import org.slf4j.LoggerFactory;
*
* @since solr 4.0
*/
-public final class ZookeeperInfoServlet extends HttpServlet {
+public final class ZookeeperInfoServlet extends BaseSolrServlet {
static final Logger log = LoggerFactory.getLogger(ZookeeperInfoServlet.class);
+
+ // used for custom sorting collection names looking like prefix##
+ // only go out to 7 digits (which safely fits in an int)
+ private static final Pattern endsWithDigits = Pattern.compile("^(\\D*)(\\d{1,7}?)$");
+
+ /**
+ * Enumeration of ways to filter collections on the graph panel.
+ */
+ static enum FilterType {
+ none, name, status
+ }
+
+ /**
+ * Holds state of a single page of collections requested from the cloud panel.
+ */
+ static final class PageOfCollections {
+ List<String> selected;
+ int numFound = 0; // total number of matches (across all pages)
+ int start = 0;
+ int rows = -1;
+ FilterType filterType;
+ String filter;
+
+ PageOfCollections(int start, int rows, FilterType filterType, String filter) {
+ this.start = start;
+ this.rows = rows;
+ this.filterType = filterType;
+ this.filter = filter;
+ }
+
+ void selectPage(List<String> collections) {
+ numFound = collections.size();
+ // start with full set and then find the sublist for the desired selected
+ selected = collections;
+
+ if (rows > 0) { // paging desired
+ if (start > numFound)
+ start = 0; // this might happen if they applied a new filter
+
+ int lastIndex = Math.min(start+rows, numFound);
+ if (start > 0 || lastIndex < numFound)
+ selected = collections.subList(start, lastIndex);
+ }
+ }
+
+ /**
+ * Filters a list of collections by name if applicable.
+ */
+ List<String> applyNameFilter(List<String> collections) {
+
+ if (filterType != FilterType.name || filter == null)
+ return collections; // name filter doesn't apply
+
+ // typically, a user will type a prefix and then *, e.g. tj*
+ // when they really mean tj.*
+ String regexFilter = (!filter.endsWith(".*") && filter.endsWith("*"))
+ ? filter.substring(0,filter.length()-1)+".*" : filter;
+
+ // case-insensitive
+ if (!regexFilter.startsWith("(?i)"))
+ regexFilter = "(?i)"+regexFilter;
+
+ Pattern filterRegex = Pattern.compile(regexFilter);
+ List<String> filtered = new ArrayList<String>();
+ for (String next : collections) {
+ if (matches(filterRegex, next))
+ filtered.add(next);
+ }
+
+ return filtered;
+ }
+
+ /**
+ * Walk the collection state JSON object to see if it has any replicas that match
+ * the state the user is filtering by.
+ */
+ @SuppressWarnings("unchecked")
+ final boolean matchesStatusFilter(Map<String,Object> collectionState, Set<String> liveNodes) {
+
+ if (filterType != FilterType.status || filter == null || filter.length() == 0)
+ return true; // no status filter, so all match
+
+ boolean isHealthy = true; // means all replicas for all shards active
+ boolean hasDownedShard = false; // means one or more shards is down
+ boolean replicaInRecovery = false;
+
+ Map<String,Object> shards = (Map<String,Object>)collectionState.get("shards");
+ for (String shardId : shards.keySet()) {
+ boolean hasActive = false;
+ Map<String,Object> shard = (Map<String,Object>)shards.get(shardId);
+ Map<String,Object> replicas = (Map<String,Object>)shard.get("replicas");
+ for (String replicaId : replicas.keySet()) {
+ Map<String,Object> replicaState = (Map<String,Object>)replicas.get(replicaId);
+ String coreState = (String)replicaState.get("state");
+ String nodeName = (String)replicaState.get("node_name");
+
+ // state can lie to you if the node is offline, so need to reconcile with live_nodes too
+ if (!liveNodes.contains(nodeName))
+ coreState = "down"; // not on a live node, so must be down
+
+ if ("active".equals(coreState)) {
+ hasActive = true; // assumed no replicas active and found one that is for this shard
+ } else {
+ if ("recovering".equals(coreState)) {
+ replicaInRecovery = true;
+ }
+ isHealthy = false; // assumed healthy and found one replica that is not
+ }
+ }
+
+ if (!hasActive)
+ hasDownedShard = true; // this is bad
+ }
+
+ if ("healthy".equals(filter)) {
+ return isHealthy;
+ } else if ("degraded".equals(filter)) {
+ return !hasDownedShard && !isHealthy; // means no shards offline but not 100% healthy either
+ } else if ("downed_shard".equals(filter)) {
+ return hasDownedShard;
+ } else if ("recovering".equals(filter)) {
+ return !isHealthy && replicaInRecovery;
+ }
+
+ return true;
+ }
+
+ final boolean matches(final Pattern filter, final String collName) {
+ return filter.matcher(collName).matches();
+ }
+
+ String getPagingHeader() {
+ return start+"|"+rows+"|"+numFound+"|"+(filterType != null ? filterType.toString() : "")+"|"+(filter != null ? filter : "");
+ }
+
+ public String toString() {
+ return getPagingHeader();
+ }
- @Override
- public void init() {
}
+
+ /**
+ * Supports paged navigation of collections on the cloud panel. To avoid serving
+ * stale collection data, this object watches the /collections znode, which will
+ * change if a collection is added or removed.
+ */
+ static final class PagedCollectionSupport implements Watcher, Comparator<String>, OnReconnect {
+
+ // this is the full merged list of collections from ZooKeeper
+ private List<String> cachedCollections;
+
+ /**
+ * If the list of collections changes, mark the cache as stale.
+ */
+ @Override
+ public void process(WatchedEvent event) {
+ synchronized (this) {
+ cachedCollections = null;
+ }
+ }
+
+ /**
+ * Create a merged view of all collections (internal from /clusterstate.json and external from /collections/?/state.json
+ */
+ private synchronized List<String> getCollections(SolrZkClient zkClient) throws KeeperException, InterruptedException {
+ if (cachedCollections == null) {
+ // cache is stale, rebuild the full list ...
+ cachedCollections = new ArrayList<String>();
+
+ List<String> fromZk = zkClient.getChildren("/collections", this, true);
+ if (fromZk != null)
+ cachedCollections.addAll(fromZk);
+
+ // sort the final merged set of collections
+ Collections.sort(cachedCollections, this);
+ }
+
+ return cachedCollections;
+ }
+
+ /**
+ * Gets the requested page of collections after applying filters and offsets.
+ */
+ public PageOfCollections fetchPage(PageOfCollections page, SolrZkClient zkClient)
+ throws KeeperException, InterruptedException {
+
+
+ List<String> children = getCollections(zkClient);
+ page.selected = children; // start with the page being the full list
+
+ // 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
+ // data from ZK to do name filtering
+ if (page.filterType == FilterType.name && page.filter != null)
+ children = page.applyNameFilter(children);
+
+ // a little hacky ... we can't select the page when filtering by
+ // status until reading all status objects from ZK
+ if (page.filterType != FilterType.status)
+ page.selectPage(children);
+
+ return page;
+ }
+
+ @Override
+ public int compare(String left, String right) {
+ if (left == null)
+ return -1;
+
+ if (left.equals(right))
+ return 0;
+
+ // sort lexically unless the two collection names start with the same base prefix
+ // and end in a number (which is a common enough naming scheme to have direct
+ // support for it)
+ Matcher leftMatcher = endsWithDigits.matcher(left);
+ if (leftMatcher.matches()) {
+ Matcher rightMatcher = endsWithDigits.matcher(right);
+ if (rightMatcher.matches()) {
+ String leftGroup1 = leftMatcher.group(1);
+ String rightGroup1 = rightMatcher.group(1);
+ if (leftGroup1.equals(rightGroup1)) {
+ // both start with the same prefix ... compare indexes
+ // 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 left.compareTo(right);
+ }
+
+ /**
+ * Called after a ZooKeeper session expiration occurs
+ */
+ @Override
+ public void command() {
+ // we need to re-establish the watcher on the collections list after session expires
+ synchronized (this) {
+ cachedCollections = null;
+ }
+ }
+ }
+
+ private PagedCollectionSupport pagingSupport;
@Override
public void doGet(HttpServletRequest request,
@@ -73,6 +330,17 @@ public final class ZookeeperInfoServlet
throw new ServletException("Missing request attribute org.apache.solr.CoreContainer.");
}
+ 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)
+ zkController.addOnReconnectListener(pagingSupport);
+ }
+ }
+ }
+
final SolrParams params;
try {
params = SolrRequestParsers.DEFAULT.parse(null, request.getServletPath(), request).getParams();
@@ -97,7 +365,25 @@ public final class ZookeeperInfoServlet
String dumpS = params.get("dump");
boolean dump = dumpS != null && dumpS.equals("true");
-
+
+ int start = paramAsInt("start", params, 0);
+ int rows = paramAsInt("rows", params, -1);
+
+ String filterType = params.get("filterType");
+ if (filterType != null) {
+ filterType = filterType.trim().toLowerCase(Locale.ROOT);
+ if (filterType.length() == 0)
+ filterType = null;
+ }
+ FilterType type = (filterType != null) ? FilterType.valueOf(filterType) : FilterType.none;
+
+ String filter = (type != FilterType.none) ? params.get("filter") : null;
+ if (filter != null) {
+ filter = filter.trim();
+ if (filter.length() == 0)
+ filter = null;
+ }
+
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
@@ -106,14 +392,17 @@ public final class ZookeeperInfoServlet
ZKPrinter printer = new ZKPrinter(response, out, cores.getZkController(), addr);
printer.detail = detail;
printer.dump = dump;
- printer.isTreeView = (params.get("wt") == null); // this is hacky but tree view requests don't come in with the wt set
+ boolean isGraphView = "graph".equals(params.get("view"));
+ printer.page = (isGraphView && "/clusterstate.json".equals(path))
+ ? new PageOfCollections(start, rows, type, filter) : null;
+ printer.pagingSupport = pagingSupport;
try {
printer.print(path);
} finally {
printer.close();
}
-
+
out.flush();
}
@@ -124,6 +413,21 @@ public final class ZookeeperInfoServlet
doGet(request, response);
}
+ protected int paramAsInt(final String paramName, final SolrParams params, final int defaultVal) {
+ int val = defaultVal;
+ String paramS = params.get(paramName);
+ if (paramS != null) {
+ String trimmed = paramS.trim();
+ if (trimmed.length() > 0) {
+ try {
+ val = Integer.parseInt(trimmed);
+ } catch (NumberFormatException nfe) {
+ log.warn("Invalid value "+paramS+" passed for parameter "+paramName+"; expected integer!");
+ }
+ }
+ }
+ return val;
+ }
//--------------------------------------------------------------------------------------
//
@@ -136,9 +440,7 @@ public final class ZookeeperInfoServlet
boolean fullpath = FULLPATH_DEFAULT;
boolean detail = false;
boolean dump = false;
-
- boolean isTreeView = false;
-
+
String addr; // the address passed to us
String keeperAddr; // the address we're connected to
@@ -150,8 +452,13 @@ public final class ZookeeperInfoServlet
int level;
int maxData = 95;
+
+ PageOfCollections page;
+ PagedCollectionSupport pagingSupport;
+ ZkController zkController;
public ZKPrinter(HttpServletResponse response, Writer out, ZkController controller, String addr) throws IOException {
+ this.zkController = controller;
this.response = response;
this.out = out;
this.addr = addr;
@@ -370,13 +677,12 @@ public final class ZookeeperInfoServlet
@SuppressWarnings("unchecked")
boolean printZnode(JSONWriter json, String path) throws IOException {
- try {
- Stat stat = new Stat();
- // Trickily, the call to zkClient.getData fills in the stat variable
- byte[] data = zkClient.getData(path, null, stat, true);
-
+ try {
String dataStr = null;
String dataStrErr = null;
+ Stat stat = new Stat();
+ // Trickily, the call to zkClient.getData fills in the stat variable
+ byte[] data = zkClient.getData(path, null, stat, true);
if (null != data) {
try {
dataStr = (new BytesRef(data)).utf8ToString();
@@ -384,41 +690,89 @@ public final class ZookeeperInfoServlet
dataStrErr = "data is not parsable as a utf8 String: " + e.toString();
}
}
- // pull in external collections too
- if (ZkStateReader.CLUSTER_STATE.equals(path) && !isTreeView) {
- SortedMap<String,Object> collectionStates = null;
- List<String> children = zkClient.getChildren(ZkStateReader.COLLECTIONS_ZKNODE, null, true);
- java.util.Collections.sort(children);
- for (String collection : children) {
- String collStatePath = ZkStateReader.getCollectionPath(collection);
- String childDataStr = null;
+ // support paging of the collections graph view (in case there are many collections)
+ if (page != null) {
+ // we've already pulled the data for /clusterstate.json from ZooKeeper above,
+ // but it needs to be parsed into a map so we can lookup collection states before
+ // trying to find them in the /collections/?/state.json znode
+ Map<String,Object> clusterstateJsonMap = null;
+ if (dataStr != null) {
try {
- byte[] childData = zkClient.getData(collStatePath, null, null, true);
- if (childData != null) {
- childDataStr = (new BytesRef(childData)).utf8ToString();
- }
- } catch (KeeperException.NoNodeException nne) {
- // safe to ignore
- } catch (Exception childErr) {
- log.error("Failed to get "+collStatePath+" due to: "+childErr);
+ clusterstateJsonMap = (Map<String, Object>) ObjectBuilder.fromJSON(dataStr);
+ } catch (Exception e) {
+ throw new SolrException(ErrorCode.SERVER_ERROR,
+ "Failed to parse /clusterstate.json from ZooKeeper due to: " + e, e);
}
-
- if (childDataStr != null) {
- if (collectionStates == null) {
- // initialize lazily as there may not be any external collections
- collectionStates = new TreeMap<>();
-
- // add the internal collections
- if (dataStr != null)
- collectionStates.putAll((Map<String,Object>)ObjectBuilder.fromJSON(dataStr));
+ } else {
+ clusterstateJsonMap = ZkNodeProps.makeMap();
+ }
+
+ // fetch the requested page of collections and then retrieve the state for each
+ page = 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<String>() : null;
+ Set<String> liveNodes = applyStatusFilter ?
+ zkController.getZkStateReader().getClusterState().getLiveNodes() : null;
+
+ SortedMap<String,Object> collectionStates = new TreeMap<String,Object>(pagingSupport);
+ for (String collection : page.selected) {
+ Object collectionState = clusterstateJsonMap.get(collection);
+ if (collectionState != null) {
+ // collection state was in /clusterstate.json
+ if (applyStatusFilter) {
+ // verify this collection matches the status filter
+ if (page.matchesStatusFilter((Map<String,Object>)collectionState,liveNodes)) {
+ matchesStatusFilter.add(collection);
+ collectionStates.put(collection, collectionState);
+ }
+ } else {
+ collectionStates.put(collection, collectionState);
+ }
+ } else {
+ // looks like an external collection ...
+ String collStatePath = String.format(Locale.ROOT, "/collections/%s/state.json", collection);
+ String childDataStr = null;
+ try {
+ byte[] childData = zkClient.getData(collStatePath, null, null, true);
+ if (childData != null)
+ childDataStr = (new BytesRef(childData)).utf8ToString();
+ } catch (KeeperException.NoNodeException nne) {
+ log.warn("State for collection "+collection+
+ " not found in /clusterstate.json or /collections/"+collection+"/state.json!");
+ } catch (Exception childErr) {
+ log.error("Failed to get "+collStatePath+" due to: "+childErr);
}
-
- // now add in the external collections
- Map<String,Object> extColl = (Map<String,Object>)ObjectBuilder.fromJSON(childDataStr);
- collectionStates.put(collection, extColl.get(collection));
- }
+
+ if (childDataStr != null) {
+ Map<String,Object> extColl = (Map<String,Object>)ObjectBuilder.fromJSON(childDataStr);
+ collectionState = extColl.get(collection);
+
+ if (applyStatusFilter) {
+ // verify this collection matches the filtered state
+ if (page.matchesStatusFilter((Map<String,Object>)collectionState,liveNodes)) {
+ matchesStatusFilter.add(collection);
+ collectionStates.put(collection, collectionState);
+ }
+ } else {
+ collectionStates.put(collection, 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;
+ }
+
if (collectionStates != null) {
CharArr out = new CharArr();
new JSONWriter(out, 2).write(collectionStates);
@@ -455,6 +809,11 @@ public final class ZookeeperInfoServlet
if (null != dataStr) {
writeKeyValue(json, "data", dataStr, false);
}
+
+ if (page != null) {
+ writeKeyValue(json, "paging", page.getPagingHeader(), false);
+ }
+
json.endObject();
} catch (KeeperException e) {
writeError(500, e.toString());
Modified: lucene/dev/branches/branch_5x/solr/webapp/web/css/styles/cloud.css
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/webapp/web/css/styles/cloud.css?rev=1648630&r1=1648629&r2=1648630&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/webapp/web/css/styles/cloud.css (original)
+++ lucene/dev/branches/branch_5x/solr/webapp/web/css/styles/cloud.css Tue Dec 30 23:16:11 2014
@@ -401,3 +401,10 @@ limitations under the License.
{
stroke: #fff;
}
+
+#cloudGraphPaging
+{
+ display: inline-block,
+ padding-top: 15px,
+ padding-bottom: 15px
+}
\ No newline at end of file
Modified: lucene/dev/branches/branch_5x/solr/webapp/web/js/scripts/cloud.js
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/webapp/web/js/scripts/cloud.js?rev=1648630&r1=1648629&r2=1648630&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/webapp/web/js/scripts/cloud.js (original)
+++ lucene/dev/branches/branch_5x/solr/webapp/web/js/scripts/cloud.js Tue Dec 30 23:16:11 2014
@@ -350,6 +350,106 @@ var generate_rgraph = function( graph_el
);
};
+var prepare_graph_data = function( response, graph_element, live_nodes, callback )
+{
+ var state = null;
+ eval( 'state = ' + response.znode.data + ';' );
+
+ var leaf_count = 0;
+ var graph_data = {
+ name: null,
+ children : []
+ };
+
+ for( var c in state )
+ {
+ var shards = [];
+ for( var s in state[c].shards )
+ {
+ var nodes = [];
+ for( var n in state[c].shards[s].replicas )
+ {
+ leaf_count++;
+ var replica = state[c].shards[s].replicas[n]
+
+ var uri = replica.base_url;
+ var parts = uri.match( /^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/ );
+ var uri_parts = {
+ protocol: parts[1],
+ host: parts[2],
+ hostname: parts[3],
+ port: parseInt( parts[5] || 80, 10 ),
+ pathname: parts[6]
+ };
+
+ helper_data.protocol.push( uri_parts.protocol );
+ helper_data.host.push( uri_parts.host );
+ helper_data.hostname.push( uri_parts.hostname );
+ helper_data.port.push( uri_parts.port );
+ helper_data.pathname.push( uri_parts.pathname );
+
+ var status = replica.state;
+
+ if( !live_nodes[replica.node_name] )
+ {
+ status = 'gone';
+ }
+
+ var node = {
+ name: uri,
+ data: {
+ type : 'node',
+ state : status,
+ leader : 'true' === replica.leader,
+ uri : uri_parts
+ }
+ };
+ nodes.push( node );
+ }
+
+ var shard = {
+ name: s,
+ data: {
+ type : 'shard'
+ },
+ children: nodes
+ };
+ shards.push( shard );
+ }
+
+ var collection = {
+ name: c,
+ data: {
+ type : 'collection'
+ },
+ children: shards
+ };
+ graph_data.children.push( collection );
+ }
+
+ helper_data.protocol = $.unique( helper_data.protocol );
+ helper_data.host = $.unique( helper_data.host );
+ helper_data.hostname = $.unique( helper_data.hostname );
+ helper_data.port = $.unique( helper_data.port );
+ helper_data.pathname = $.unique( helper_data.pathname );
+
+ callback( graph_element, graph_data, leaf_count );
+}
+
+var update_status_filter = function(filterType, filterVal) {
+ if (filterType == 'status') {
+ $( '#cloudGraphPagingStatusFilter' ).val(filterVal);
+ $( '#cloudGraphPagingStatusFilter' ).show();
+ $( '#cloudGraphPagingFilter' ).hide();
+ $( '#cloudGraphPagingFilter' ).val('');
+ } else {
+ $( '#cloudGraphPagingStatusFilter' ).hide();
+ $( '#cloudGraphPagingStatusFilter' ).val('');
+ $( '#cloudGraphPagingFilter' ).val(filterVal);
+ $( '#cloudGraphPagingFilter' ).show();
+ }
+};
+
var prepare_graph = function( graph_element, callback )
{
$.ajax
@@ -365,101 +465,82 @@ var prepare_graph = function( graph_elem
live_nodes[response.tree[0].children[c].data.title] = true;
}
+ var start = $( '#cloudGraphPagingStart' ).val();
+ var rows = $( '#cloudGraphPagingRows' ).val();
+ var clusterStateUrl = app.config.solr_path + '/zookeeper?wt=json&detail=true&path=%2Fclusterstate.json&view=graph';
+ if (start && rows)
+ clusterStateUrl += ('&start='+start+'&rows='+rows);
+
+ var filterType = $( '#cloudGraphPagingFilterType' ).val();
+ if (filterType) {
+ var filter = (filterType == 'status')
+ ? $( '#cloudGraphPagingStatusFilter' ).val()
+ : $( '#cloudGraphPagingFilter' ).val();
+ if (filter)
+ clusterStateUrl += ('&filterType='+filterType+'&filter='+filter);
+ }
+
$.ajax
(
{
- url : app.config.solr_path + '/zookeeper?wt=json&detail=true&path=%2Fclusterstate.json',
+ url : clusterStateUrl,
dataType : 'json',
context : graph_element,
beforeSend : function( xhr, settings )
{
- this
- .show();
+ this.show();
},
success : function( response, text_status, xhr )
- {
- var state = null;
- eval( 'state = ' + response.znode.data + ';' );
-
- var leaf_count = 0;
- var graph_data = {
- name: null,
- children : []
- };
-
- for( var c in state )
- {
- var shards = [];
- for( var s in state[c].shards )
- {
- var nodes = [];
- for( var n in state[c].shards[s].replicas )
- {
- leaf_count++;
- var replica = state[c].shards[s].replicas[n]
+ {
+ prepare_graph_data(response, graph_element, live_nodes, callback)
- var uri = replica.base_url;
- var parts = uri.match( /^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/ );
- var uri_parts = {
- protocol: parts[1],
- host: parts[2],
- hostname: parts[3],
- port: parseInt( parts[5] || 80, 10 ),
- pathname: parts[6]
- };
-
- helper_data.protocol.push( uri_parts.protocol );
- helper_data.host.push( uri_parts.host );
- helper_data.hostname.push( uri_parts.hostname );
- helper_data.port.push( uri_parts.port );
- helper_data.pathname.push( uri_parts.pathname );
-
- var status = replica.state;
-
- if( !live_nodes[replica.node_name] )
- {
- status = 'gone';
- }
-
- var node = {
- name: uri,
- data: {
- type : 'node',
- state : status,
- leader : 'true' === replica.leader,
- uri : uri_parts
- }
- };
- nodes.push( node );
- }
-
- var shard = {
- name: s,
- data: {
- type : 'shard'
- },
- children: nodes
- };
- shards.push( shard );
+ if (response.znode && response.znode.paging) {
+ var parr = response.znode.paging.split('|');
+ if (parr.length < 3) {
+ $( '#cloudGraphPaging' ).hide();
+ return;
}
-
- var collection = {
- name: c,
- data: {
- type : 'collection'
- },
- children: shards
- };
- graph_data.children.push( collection );
- }
-
- helper_data.protocol = $.unique( helper_data.protocol );
- helper_data.host = $.unique( helper_data.host );
- helper_data.hostname = $.unique( helper_data.hostname );
- helper_data.port = $.unique( helper_data.port );
- helper_data.pathname = $.unique( helper_data.pathname );
-
- callback( graph_element, graph_data, leaf_count );
+
+ var start = Math.max(parseInt(parr[0]),0);
+ var prevEnabled = (start > 0);
+ $('#cloudGraphPagingPrev').prop('disabled', !prevEnabled);
+ if (prevEnabled)
+ $('#cloudGraphPagingPrev').show();
+ else
+ $('#cloudGraphPagingPrev').hide();
+
+ var rows = parseInt(parr[1])
+ var total = parseInt(parr[2])
+ $( '#cloudGraphPagingStart' ).val(start);
+ $( '#cloudGraphPagingRows' ).val(rows);
+ if (rows == -1)
+ $( '#cloudGraphPaging' ).hide();
+
+ var filterType = parr.length > 3 ? parr[3] : '';
+ if (filterType == '' || filterType == 'none') filterType = 'status';
+
+ $( '#cloudGraphPagingFilterType' ).val(filterType);
+ var filter = parr.length > 4 ? parr[4] : '';
+
+ update_status_filter(filterType, filter);
+
+ var page = Math.floor(start/rows)+1;
+ var pages = Math.ceil(total/rows);
+ var last = Math.min(start+rows,total);
+ var nextEnabled = (last < total);
+ $('#cloudGraphPagingNext').prop('disabled', !nextEnabled);
+ if (nextEnabled)
+ $('#cloudGraphPagingNext').show();
+ else
+ $('#cloudGraphPagingNext').hide();
+
+ var status = (total > 0)
+ ? 'Collections '+(start+1)+' - '+last+' of '+total+'. '
+ : 'No collections found.';
+ $( '#cloudGraphPagingStatus' ).html(status);
+ } else {
+ $( '#cloudGraphPaging' ).hide();
+ }
},
error : function( xhr, text_status, error_thrown)
{
@@ -662,6 +743,21 @@ var init_tree = function( tree_element )
);
};
+// updates the starting position for paged navigation
+// and then rebuilds the graph based on the selected page
+var update_start = function(direction, cloud_element) {
+ var start = $( '#cloudGraphPagingStart' ).val();
+ var rows = $( '#cloudGraphPagingRows' ).val();
+ var startAt = start ? parseInt(start) : 0;
+ var numRows = rows ? parseInt(rows) : 20;
+ var newStart = Math.max(startAt + (rows * direction),0);
+ $( '#cloudGraphPagingStart' ).val(newStart);
+
+ var graph_element = $( '#graph-content', cloud_element );
+ $( '#canvas', graph_element).empty();
+ init_graph( graph_element );
+};
+
// #/~cloud
sammy.get
(
@@ -704,6 +800,45 @@ sammy.get
{
$( this ).addClass( 'active' );
init_graph( $( '#graph-content', cloud_element ) );
+
+ $('#cloudGraphPagingNext').click(function() {
+ update_start(1, cloud_element);
+ });
+
+ $('#cloudGraphPagingPrev').click(function() {
+ update_start(-1, cloud_element);
+ });
+
+ $('#cloudGraphPagingRows').change(function() {
+ var rows = $( this ).val();
+ if (!rows || rows == '')
+ $( this ).val("20");
+
+ // ? restart the start position when rows changes?
+ $( '#cloudGraphPagingStart' ).val(0);
+ update_start(-1, cloud_element);
+ });
+
+ $('#cloudGraphPagingFilter').change(function() {
+ var filter = $( this ).val();
+ // reset the start position when the filter changes
+ $( '#cloudGraphPagingStart' ).val(0);
+ update_start(-1, cloud_element);
+ });
+
+ $( '#cloudGraphPagingStatusFilter' ).show();
+ $( '#cloudGraphPagingFilter' ).hide();
+
+ $('#cloudGraphPagingFilterType').change(function() {
+ update_status_filter($( this ).val(), '');
+ });
+
+ $('#cloudGraphPagingStatusFilter').change(function() {
+ // just reset the paged navigation controls based on this update
+ $( '#cloudGraphPagingStart' ).val(0);
+ update_start(-1, cloud_element);
+ });
+
}
);
@@ -714,6 +849,8 @@ sammy.get
'activate',
function( event )
{
+ $( "#cloudGraphPaging" ).hide(); // TODO: paging for rgraph too
+
$( this ).addClass( 'active' );
init_rgraph( $( '#graph-content', cloud_element ) );
}
Modified: lucene/dev/branches/branch_5x/solr/webapp/web/tpl/cloud.html
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_5x/solr/webapp/web/tpl/cloud.html?rev=1648630&r1=1648629&r2=1648630&view=diff
==============================================================================
--- lucene/dev/branches/branch_5x/solr/webapp/web/tpl/cloud.html (original)
+++ lucene/dev/branches/branch_5x/solr/webapp/web/tpl/cloud.html Tue Dec 30 23:16:11 2014
@@ -50,6 +50,28 @@ limitations under the License.
</ul>
</div>
+ <div style="width: 100%; text-align: center;">
+ <div id="cloudGraphPaging">
+ <button id="cloudGraphPagingPrev">< Previous</button>
+ <input id="cloudGraphPagingStart" type="hidden" name="start" />
+ <span id="cloudGraphPagingStatus"></span>
+ Filter by: <select id="cloudGraphPagingFilterType">
+ <option value="status">Status</option>
+ <option value="name">Name</option>
+ </select>
+ <select id="cloudGraphPagingStatusFilter">
+ <option value=""> - Any - </option>
+ <option value="healthy">Healthy</option>
+ <option value="degraded">Degraded</option>
+ <option value="downed_shard">Downed Shard</option>
+ <option value="recovering">Replica in Recovery</option>
+ </select>
+ <input id="cloudGraphPagingFilter" type="text" size="10" name="filter" />
+ Show <input id="cloudGraphPagingRows" type="text" size="2" name="rows" /> per page.
+ <button id="cloudGraphPagingNext">Next ></button>
+ </div>
+ </div>
+
</div>
</div>
@@ -62,4 +84,4 @@ limitations under the License.
<pre class="debug"></pre>
</div>
-</div>
\ No newline at end of file
+</div>