blob: 9130fcc317ac7ace595a26779360ef99344f7a62 [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.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.PerReplicaStates;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
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);
}
public void getClusterStatus(NamedList<Object> 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);
}
}
}
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();
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));
}
byte[] bytes = Utils.toJSON(clusterStateCollection);
@SuppressWarnings("unchecked")
Map<String, Object> docCollection = (Map<String, Object>) Utils.fromJSON(bytes);
collectionStatus = getCollectionStatus(docCollection, name, requestedShards);
collectionStatus.put("znodeVersion", clusterStateCollection.getZNodeVersion());
if (collectionVsAliases.containsKey(name) && !collectionVsAliases.get(name).isEmpty()) {
collectionStatus.put("aliases", collectionVsAliases.get(name));
}
String configName = clusterStateCollection.getConfigName();
collectionStatus.put("configName", configName);
if (message.getBool("prs", false) && clusterStateCollection.isPerReplicaState()) {
PerReplicaStates prs = clusterStateCollection.getPerReplicaStates();
collectionStatus.put("PRS", prs);
}
collectionProps.add(name, collectionStatus);
}
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
Map<String, Object> 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) {
for (Map.Entry<String, Object> next : collectionProps) {
Map<String, Object> collMap = (Map<String, Object>) next.getValue();
Map<String, Object> shards = (Map<String, Object>) collMap.get("shards");
for (Object nextShard : shards.values()) {
Map<String, Object> shardMap = (Map<String, Object>) nextShard;
Map<String, Object> replicas = (Map<String, Object>) shardMap.get("replicas");
for (Object nextReplica : replicas.values()) {
Map<String, Object> replicaMap = (Map<String, Object>) nextReplica;
if (Replica.State.getState((String) replicaMap.get(ZkStateReader.STATE_PROP))
!= Replica.State.DOWN) {
// not down, so verify the node is live
String node_name = (String) replicaMap.get(ZkStateReader.NODE_NAME_PROP);
if (!liveNodes.contains(node_name)) {
// node is not live, so this replica is actually down
replicaMap.put(ZkStateReader.STATE_PROP, Replica.State.DOWN.toString());
}
}
}
}
}
}
@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;
}
}
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;
}
}