blob: 6942e3b8ce0a099ecab4479b5fd1ff8dada88889 [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.cloud;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.ZkStateReader;
import org.apache.solr.common.util.Utils;
/**
* A utility class that can create mock ZkStateReader objects with custom ClusterState objects created
* using a simple string based description. See {@link #buildClusterState(String, int, int, String...)} for
* details on how the cluster state can be created.
*
* @lucene.experimental
*/
public class ClusterStateMockUtil {
private final static Pattern BLUEPRINT = Pattern.compile("([a-z])(\\d+)?(?:(['A','R','D','F']))?(\\*)?");
public static ZkStateReader buildClusterState(String clusterDescription, String ... liveNodes) {
return buildClusterState(clusterDescription, 1, liveNodes);
}
public static ZkStateReader buildClusterState(String clusterDescription, int replicationFactor, String ... liveNodes) {
return buildClusterState(clusterDescription, replicationFactor, 10, liveNodes);
}
/**
* This method lets you construct a complex ClusterState object by using simple strings of letters.
*
* c = collection, s = slice, r = replica (nrt type, default), n = nrt replica, t = tlog replica, p = pull replica, \d = node number (r2 means the replica is on node 2),
* state = [A,R,D,F], * = replica to replace, binds to the left.
*
* For example:
* csrr2rD*sr2csr
*
* Creates:
*
* 'csrr2rD*'
* A collection, a shard, a replica on node 1 (the default) that is active (the default), a replica on node 2, and a replica on node 1
* that has a state of down and is the replica we will be looking to put somewhere else (the *).
*
* 'sr2'
* Then, another shard that has a replica on node 2.
*
* 'csr'
* Then, another collection that has a shard with a single active replica on node 1.
*
* Result:
* {
* "collection2":{
* "maxShardsPerNode":"1",
* "replicationFactor":"1",
* "shards":{"slice1":{
* "state":"active",
* "replicas":{"replica5":{
* "state":"active",
* "node_name":"baseUrl1_",
* "base_url":"http://baseUrl1"}}}}},
* "collection1":{
* "maxShardsPerNode":"1",
* "replicationFactor":"1",
* "shards":{
* "slice1":{
* "state":"active",
* "replicas":{
* "replica3 (bad)":{
* "state":"down",
* "node_name":"baseUrl1_",
* "base_url":"http://baseUrl1"},
* "replica2":{
* "state":"active",
* "node_name":"baseUrl2_",
* "base_url":"http://baseUrl2"},
* "replica1":{
* "state":"active",
* "node_name":"baseUrl1_",
* "base_url":"http://baseUrl1"}}},
* "slice2":{
* "state":"active",
* "replicas":{"replica4":{
* "state":"active",
* "node_name":"baseUrl2_",
* "base_url":"http://baseUrl2"}}}}}}
*
*/
@SuppressWarnings("resource")
public static ZkStateReader buildClusterState(String clusterDescription, int replicationFactor, int maxShardsPerNode, String ... liveNodes) {
Map<String,Slice> slices = null;
Map<String,Replica> replicas = null;
Map<String,Object> collectionProps = new HashMap<>();
collectionProps.put(ZkStateReader.MAX_SHARDS_PER_NODE, Integer.toString(maxShardsPerNode));
collectionProps.put(ZkStateReader.REPLICATION_FACTOR, Integer.toString(replicationFactor));
Map<String,DocCollection> collectionStates = new HashMap<>();
DocCollection docCollection = null;
String collName = null;
String sliceName = null;
Slice slice = null;
int replicaCount = 1;
Matcher m = BLUEPRINT.matcher(clusterDescription);
while (m.find()) {
Replica replica;
switch (m.group(1)) {
case "c":
slices = new HashMap<>();
docCollection = new DocCollection(collName = "collection" + (collectionStates.size() + 1), slices, collectionProps, DocRouter.DEFAULT);
collectionStates.put(docCollection.getName(), docCollection);
break;
case "s":
replicas = new HashMap<>();
if(collName == null) collName = "collection" + (collectionStates.size() + 1);
slice = new Slice(sliceName = "slice" + (slices.size() + 1), replicas, null, collName);
slices.put(slice.getName(), slice);
// hack alert: the DocCollection constructor copies over active slices to its active slice map in the constructor
// but here we construct the DocCollection before creating the slices which breaks code that calls DocCollection.getActiveSlices
// so here we re-create doc collection with the latest slices map to workaround this problem
// todo: a better fix would be to have a builder class for DocCollection that builds the final object once all the slices and replicas have been created.
docCollection = docCollection.copyWithSlices(slices);
collectionStates.put(docCollection.getName(), docCollection);
break;
case "r":
case "n":
case "t":
case "p":
String node = m.group(2);
String replicaName = "replica" + replicaCount++;
String stateCode = m.group(3);
Map<String, Object> replicaPropMap = makeReplicaProps(sliceName, node, replicaName, stateCode, m.group(1));
if (collName == null) collName = "collection" + (collectionStates.size() + 1);
if (sliceName == null) collName = "slice" + (slices.size() + 1);
// O(n^2) alert! but this is for mocks and testing so shouldn't be used for very large cluster states
boolean leaderFound = false;
for (Map.Entry<String, Replica> entry : replicas.entrySet()) {
Replica value = entry.getValue();
if ("true".equals(value.get(Slice.LEADER))) {
leaderFound = true;
break;
}
}
if (!leaderFound && !m.group(1).equals("p")) {
replicaPropMap.put(Slice.LEADER, "true");
}
replica = new Replica(replicaName, replicaPropMap, collName, sliceName);
replicas.put(replica.getName(), replica);
// hack alert: re-create slice with existing data and new replicas map so that it updates its internal leader attribute
slice = new Slice(slice.getName(), replicas, null, collName);
slices.put(slice.getName(), slice);
// we don't need to update doc collection again because we aren't adding a new slice or changing its state
break;
default:
break;
}
}
ClusterState clusterState = new ClusterState(1, new HashSet<>(Arrays.asList(liveNodes)), collectionStates);
MockZkStateReader reader = new MockZkStateReader(clusterState, collectionStates.keySet());
String json;
json = new String(Utils.toJSON(clusterState), StandardCharsets.UTF_8);
System.err.println(json);
return reader;
}
private static Map<String, Object> makeReplicaProps(String sliceName, String node, String replicaName, String stateCode, String replicaTypeCode) {
if (node == null || node.trim().length() == 0) {
node = "1";
}
Replica.State state = Replica.State.ACTIVE;
if (stateCode != null) {
switch (stateCode.charAt(0)) {
case 'S':
state = Replica.State.ACTIVE;
break;
case 'R':
state = Replica.State.RECOVERING;
break;
case 'D':
state = Replica.State.DOWN;
break;
case 'F':
state = Replica.State.RECOVERY_FAILED;
break;
default:
throw new IllegalArgumentException(
"Unexpected state for replica: " + stateCode);
}
}
Replica.Type replicaType = Replica.Type.NRT;
switch (replicaTypeCode) {
case "t":
replicaType = Replica.Type.TLOG;
break;
case "p":
replicaType = Replica.Type.PULL;
break;
}
Map<String,Object> replicaPropMap = new HashMap<>();
int port = 8982 + Integer.parseInt(node);
String nodeName = String.format(Locale.ROOT, "baseUrl%s:%d_", node, port);
replicaPropMap.put(ZkStateReader.NODE_NAME_PROP, nodeName);
replicaPropMap.put(ZkStateReader.STATE_PROP, state.toString());
replicaPropMap.put(ZkStateReader.CORE_NAME_PROP, sliceName + "_" + replicaName);
replicaPropMap.put(ZkStateReader.REPLICA_TYPE, replicaType.name());
return replicaPropMap;
}
}