blob: 80c2b2d099fb396a0de79dccbb86b4d0df426cf5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.admin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.Aliases;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.DocRouter;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.UrlScheme;
import org.apache.solr.common.cloud.ZkNodeProps;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.ShardParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.common.util.Utils;
import org.apache.zookeeper.KeeperException;
public class ClusterStatus {
private final ZkStateReader zkStateReader;
private final ZkNodeProps message;
private final String collection; // maybe null
/** Shard / collection health state. */
public enum Health {
/** All replicas up, leader exists. */
GREEN,
/** Some replicas down, leader exists. */
YELLOW,
/** Most replicas down, leader exists. */
ORANGE,
/** No leader or all replicas down. */
RED;
public static final float ORANGE_LEVEL = 0.5f;
public static final float RED_LEVEL = 0.0f;
public static Health calcShardHealth(float fractionReplicasUp, boolean hasLeader) {
if (hasLeader) {
if (fractionReplicasUp == 1.0f) {
return GREEN;
} else if (fractionReplicasUp > ORANGE_LEVEL) {
return YELLOW;
} else if (fractionReplicasUp > RED_LEVEL) {
return ORANGE;
} else {
return RED;
}
} else {
return RED;
}
}
/** Combine multiple states into one. Always reports as the worst state. */
public static Health combine(Collection<Health> states) {
Health res = GREEN;
for (Health state : states) {
if (state.ordinal() > res.ordinal()) {
res = state;
}
}
return res;
}
}
public ClusterStatus(ZkStateReader zkStateReader, ZkNodeProps props) {
this.zkStateReader = zkStateReader;
this.message = props;
collection = props.getStr(ZkStateReader.COLLECTION_PROP);
}
@SuppressWarnings("unchecked")
public void getClusterStatus(@SuppressWarnings({"rawtypes"})NamedList results)
throws KeeperException, InterruptedException {
// read aliases
Aliases aliases = zkStateReader.getAliases();
Map<String, List<String>> collectionVsAliases = new HashMap<>();
Map<String, List<String>> aliasVsCollections = aliases.getCollectionAliasListMap();
for (Map.Entry<String, List<String>> entry : aliasVsCollections.entrySet()) {
String alias = entry.getKey();
List<String> colls = entry.getValue();
for (String coll : colls) {
if (collection == null || collection.equals(coll)) {
List<String> list = collectionVsAliases.computeIfAbsent(coll, k -> new ArrayList<>());
list.add(alias);
}
}
}
@SuppressWarnings({"rawtypes"})
Map roles = null;
if (zkStateReader.getZkClient().exists(ZkStateReader.ROLES, true)) {
roles = (Map) Utils.fromJSON(zkStateReader.getZkClient().getData(ZkStateReader.ROLES, null, null, true));
}
ClusterState clusterState = zkStateReader.getClusterState();
// convert cluster state into a map of writable types
byte[] bytes = Utils.toJSON(clusterState);
Map<String, Object> stateMap = (Map<String,Object>) Utils.fromJSON(bytes);
String routeKey = message.getStr(ShardParams._ROUTE_);
String shard = message.getStr(ZkStateReader.SHARD_ID_PROP);
Map<String, DocCollection> collectionsMap = null;
if (collection == null) {
collectionsMap = clusterState.getCollectionsMap();
} else {
collectionsMap = Collections.singletonMap(collection, clusterState.getCollectionOrNull(collection));
}
boolean isAlias = aliasVsCollections.containsKey(collection);
boolean didNotFindCollection = collectionsMap.get(collection) == null;
if (didNotFindCollection && isAlias) {
// In this case this.collection is an alias name not a collection
// get all collections and filter out collections not in the alias
collectionsMap = clusterState.getCollectionsMap().entrySet().stream()
.filter((entry) -> aliasVsCollections.get(collection).contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
NamedList<Object> collectionProps = new SimpleOrderedMap<>();
for (Map.Entry<String, DocCollection> entry : collectionsMap.entrySet()) {
Map<String, Object> collectionStatus;
String name = entry.getKey();
DocCollection clusterStateCollection = entry.getValue();
if (clusterStateCollection == null) {
if (collection != null) {
SolrException solrException = new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Collection: " + name + " not found");
solrException.setMetadata("CLUSTERSTATUS","NOT_FOUND");
throw solrException;
} else {
//collection might have got deleted at the same time
continue;
}
}
Set<String> requestedShards = new HashSet<>();
if (routeKey != null) {
DocRouter router = clusterStateCollection.getRouter();
Collection<Slice> slices = router.getSearchSlices(routeKey, null, clusterStateCollection);
for (Slice slice : slices) {
requestedShards.add(slice.getName());
}
}
if (shard != null) {
String[] paramShards = shard.split(",");
requestedShards.addAll(Arrays.asList(paramShards));
}
if (clusterStateCollection.getStateFormat() > 1) {
bytes = Utils.toJSON(clusterStateCollection);
Map<String, Object> docCollection = (Map<String, Object>) Utils.fromJSON(bytes);
collectionStatus = getCollectionStatus(docCollection, name, requestedShards);
} else {
collectionStatus = getCollectionStatus((Map<String, Object>) stateMap.get(name), name, requestedShards);
}
collectionStatus.put("znodeVersion", clusterStateCollection.getZNodeVersion());
if (collectionVsAliases.containsKey(name) && !collectionVsAliases.get(name).isEmpty()) {
collectionStatus.put("aliases", collectionVsAliases.get(name));
}
try {
String configName = zkStateReader.readConfigName(name);
collectionStatus.put("configName", configName);
collectionProps.add(name, collectionStatus);
} catch (KeeperException.NoNodeException ex) {
// skip this collection because the configset's znode has been deleted
// which can happen during aggressive collection removal, see SOLR-10720
}
}
List<String> liveNodes = zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null, true);
// now we need to walk the collectionProps tree to cross-check replica state with live nodes
crossCheckReplicaStateWithLiveNodes(liveNodes, collectionProps);
NamedList<Object> clusterStatus = new SimpleOrderedMap<>();
clusterStatus.add("collections", collectionProps);
// read cluster properties
@SuppressWarnings({"rawtypes"})
Map clusterProps = zkStateReader.getClusterProperties();
if (clusterProps != null && !clusterProps.isEmpty()) {
clusterStatus.add("properties", clusterProps);
}
// add the alias map too
Map<String, String> collectionAliasMap = aliases.getCollectionAliasMap(); // comma delim
if (!collectionAliasMap.isEmpty()) {
clusterStatus.add("aliases", collectionAliasMap);
}
// add the roles map
if (roles != null) {
clusterStatus.add("roles", roles);
}
// add live_nodes
clusterStatus.add("live_nodes", liveNodes);
results.add("cluster", clusterStatus);
}
/**
* Get collection status from cluster state.
* Can return collection status by given shard name.
*
*
* @param collection collection map parsed from JSON-serialized {@link ClusterState}
* @param name collection name
* @param requestedShards a set of shards to be returned in the status.
* An empty or null values indicates <b>all</b> shards.
* @return map of collection properties
*/
@SuppressWarnings("unchecked")
private Map<String, Object> getCollectionStatus(Map<String, Object> collection, String name, Set<String> requestedShards) {
if (collection == null) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Collection: " + name + " not found");
}
if (requestedShards == null || requestedShards.isEmpty()) {
return postProcessCollectionJSON(collection);
} else {
Map<String, Object> shards = (Map<String, Object>) collection.get("shards");
Map<String, Object> selected = new HashMap<>();
for (String selectedShard : requestedShards) {
if (!shards.containsKey(selectedShard)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Collection: " + name + " shard: " + selectedShard + " not found");
}
selected.put(selectedShard, shards.get(selectedShard));
collection.put("shards", selected);
}
return postProcessCollectionJSON(collection);
}
}
/**
* Walks the tree of collection status to verify that any replicas not reporting a "down" status is
* on a live node, if any replicas reporting their status as "active" but the node is not live is
* marked as "down"; used by CLUSTERSTATUS.
* @param liveNodes List of currently live node names.
* @param collectionProps Map of collection status information pulled directly from ZooKeeper.
*/
@SuppressWarnings("unchecked")
protected void crossCheckReplicaStateWithLiveNodes(List<String> liveNodes, NamedList<Object> collectionProps) {
Iterator<Map.Entry<String,Object>> colls = collectionProps.iterator();
while (colls.hasNext()) {
Map.Entry<String,Object> next = colls.next();
Map<String,Object> collMap = (Map<String,Object>)next.getValue();
Map<String,Object> shards = (Map<String,Object>)collMap.get("shards");
for (Object nextShard : shards.values()) {
Map<String,Object> shardMap = (Map<String,Object>)nextShard;
Map<String,Object> replicas = (Map<String,Object>)shardMap.get("replicas");
for (Object nextReplica : replicas.values()) {
Map<String,Object> replicaMap = (Map<String,Object>)nextReplica;
if (Replica.State.getState((String) replicaMap.get(ZkStateReader.STATE_PROP)) != Replica.State.DOWN) {
// not down, so verify the node is live
String node_name = (String)replicaMap.get(ZkStateReader.NODE_NAME_PROP);
if (!liveNodes.contains(node_name)) {
// node is not live, so this replica is actually down
replicaMap.put(ZkStateReader.STATE_PROP, Replica.State.DOWN.toString());
}
}
}
}
}
}
@SuppressWarnings("unchecked")
public static Map<String,Object> postProcessCollectionJSON(Map<String, Object> collection) {
final Map<String, Map<String,Object>> shards = collection != null
? (Map<String, Map<String,Object>>)collection.getOrDefault("shards", Collections.emptyMap())
: Collections.emptyMap();
final List<Health> healthStates = new ArrayList<>(shards.size());
shards.forEach((shardName, s) -> {
final Map<String, Map<String,Object>> replicas =
(Map<String, Map<String,Object>>)s.getOrDefault("replicas", Collections.emptyMap());
int[] totalVsActive = new int[2];
boolean hasLeader = false;
for(Map<String, Object> r : replicas.values()) {
totalVsActive[0]++;
boolean active = false;
if (Replica.State.ACTIVE.toString().equals(r.get("state"))) {
totalVsActive[1]++;
active = true;
}
if ("true".equals(r.get("leader")) && active) {
hasLeader = true;
}
String nodeName = (String)r.get(ZkStateReader.NODE_NAME_PROP);
if (nodeName != null) {
// UI needs the base_url set
r.put(ZkStateReader.BASE_URL_PROP, UrlScheme.INSTANCE.getBaseUrlForNodeName(nodeName));
}
}
float ratioActive;
if (totalVsActive[0] == 0) {
ratioActive = 0.0f;
} else {
ratioActive = (float) totalVsActive[1] / totalVsActive[0];
}
Health health = Health.calcShardHealth(ratioActive, hasLeader);
s.put("health", health.toString());
healthStates.add(health);
});
collection.put("health", Health.combine(healthStates).toString());
return collection;
}
}