mirror of https://github.com/apache/lucene.git
SOLR-5810: basic paged nav support for cloud graph panel for many collections.
git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1648621 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
04bee476e1
commit
e18538cc4b
|
@ -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);
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 extends HttpServlet {
|
|||
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 extends HttpServlet {
|
|||
|
||||
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 extends HttpServlet {
|
|||
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 extends HttpServlet {
|
|||
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 extends HttpServlet {
|
|||
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 extends HttpServlet {
|
|||
|
||||
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 extends HttpServlet {
|
|||
|
||||
@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 extends HttpServlet {
|
|||
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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// now add in the external collections
|
||||
Map<String,Object> extColl = (Map<String,Object>)ObjectBuilder.fromJSON(childDataStr);
|
||||
collectionStates.put(collection, extColl.get(collection));
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
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 extends HttpServlet {
|
|||
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());
|
||||
|
|
|
@ -401,3 +401,10 @@ limitations under the License.
|
|||
{
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
#cloudGraphPaging
|
||||
{
|
||||
display: inline-block,
|
||||
padding-top: 15px,
|
||||
padding-bottom: 15px
|
||||
}
|
|
@ -350,6 +350,106 @@ var generate_rgraph = function( graph_element, graph_data, leaf_count )
|
|||
);
|
||||
};
|
||||
|
||||
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_element, callback )
|
|||
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 : []
|
||||
};
|
||||
{
|
||||
prepare_graph_data(response, graph_element, live_nodes, callback)
|
||||
|
||||
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 );
|
||||
if (response.znode && response.znode.paging) {
|
||||
var parr = response.znode.paging.split('|');
|
||||
if (parr.length < 3) {
|
||||
$( '#cloudGraphPaging' ).hide();
|
||||
return;
|
||||
}
|
||||
|
||||
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] : '';
|
||||
|
||||
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 );
|
||||
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 ) );
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue