blob: 43de56ede0df6ec9162755a77ba4f4946fc342c8 [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.cluster.placement;
import org.apache.solr.cluster.*;
import org.apache.solr.cluster.placement.impl.AttributeFetcherImpl;
import org.apache.solr.cluster.placement.impl.AttributeValuesImpl;
import org.apache.solr.cluster.placement.impl.CollectionMetricsBuilder;
import org.apache.solr.cluster.placement.impl.NodeMetricImpl;
import org.apache.solr.cluster.placement.impl.PlacementPlanFactoryImpl;
import org.apache.solr.cluster.placement.impl.ReplicaMetricImpl;
import org.apache.solr.common.util.Pair;
import org.junit.Assert;
import java.util.*;
/**
* Builder classes to make tests using different cluster and node configurations easier to write and to read.
*/
public class Builders {
public static ClusterBuilder newClusterBuilder() {
return new ClusterBuilder();
}
public static CollectionBuilder newCollectionBuilder(String collectionName) {
return new CollectionBuilder(collectionName);
}
public static class ClusterBuilder {
/**
* {@link NodeBuilder} for the live nodes of the cluster.
*/
private LinkedList<NodeBuilder> nodeBuilders = new LinkedList<>();
private LinkedList<CollectionBuilder> collectionBuilders = new LinkedList<>();
public ClusterBuilder initializeLiveNodes(int countNodes) {
nodeBuilders = new LinkedList<>();
for (int n = 0; n < countNodes; n++) {
NodeBuilder nodeBuilder = new NodeBuilder().setNodeName("node_" + n); // Default name, can be changed
nodeBuilder.setTotalDiskGB(10000.0);
nodeBuilder.setFreeDiskGB(5000.0);
nodeBuilders.add(nodeBuilder);
}
return this;
}
public LinkedList<NodeBuilder> getLiveNodeBuilders() {
return nodeBuilders;
}
public ClusterBuilder addCollection(CollectionBuilder collectionBuilder) {
collectionBuilders.add(collectionBuilder);
return this;
}
public Cluster build() {
// TODO if converting all tests to use builders change ClusterImpl ctor to use list of nodes
return new ClusterAbstractionsForTest.ClusterImpl(new HashSet<>(buildLiveNodes()), buildClusterCollections());
}
public List<Node> buildLiveNodes() {
List<Node> liveNodes = new LinkedList<>();
for (NodeBuilder nodeBuilder : nodeBuilders) {
liveNodes.add(nodeBuilder.build());
}
return liveNodes;
}
Map<String, SolrCollection> buildClusterCollections() {
Map<String, SolrCollection> clusterCollections = new LinkedHashMap<>();
for (CollectionBuilder collectionBuilder : collectionBuilders) {
SolrCollection solrCollection = collectionBuilder.build();
clusterCollections.put(solrCollection.getName(), solrCollection);
}
return clusterCollections;
}
private static final PlacementPlanFactory PLACEMENT_PLAN_FACTORY = new PlacementPlanFactoryImpl();
public PlacementContext buildPlacementContext() {
Cluster cluster = build();
AttributeFetcher attributeFetcher = buildAttributeFetcher();
return new PlacementContext() {
@Override
public Cluster getCluster() {
return cluster;
}
@Override
public AttributeFetcher getAttributeFetcher() {
return attributeFetcher;
}
@Override
public PlacementPlanFactory getPlacementPlanFactory() {
return PLACEMENT_PLAN_FACTORY;
}
};
}
public AttributeFetcher buildAttributeFetcher() {
Map<String, Map<Node, String>> sysprops = new HashMap<>();
Map<NodeMetric<?>, Map<Node, Object>> metrics = new HashMap<>();
Map<String, CollectionMetrics> collectionMetrics = new HashMap<>();
// TODO And a few more missing and will be added...
// Slight redoing of work twice (building Node instances) but let's favor readability over tricks (I could think
// of many) to reuse the nodes computed in build() or build the AttributeFetcher at the same time.
for (NodeBuilder nodeBuilder : nodeBuilders) {
Node node = nodeBuilder.build();
if (nodeBuilder.getCoreCount() != null) {
metrics.computeIfAbsent(NodeMetricImpl.NUM_CORES, n -> new HashMap<>())
.put(node, nodeBuilder.getCoreCount());
}
if (nodeBuilder.getFreeDiskGB() != null) {
metrics.computeIfAbsent(NodeMetricImpl.FREE_DISK_GB, n -> new HashMap<>())
.put(node, nodeBuilder.getFreeDiskGB());
}
if (nodeBuilder.getTotalDiskGB() != null) {
metrics.computeIfAbsent(NodeMetricImpl.TOTAL_DISK_GB, n -> new HashMap<>())
.put(node, nodeBuilder.getTotalDiskGB());
}
if (nodeBuilder.getSysprops() != null) {
nodeBuilder.getSysprops().forEach((name, value) -> {
sysprops.computeIfAbsent(name, n -> new HashMap<>())
.put(node, value);
});
}
if (nodeBuilder.getMetrics() != null) {
nodeBuilder.getMetrics().forEach((name, value) -> {
metrics.computeIfAbsent(name, n -> new HashMap<>())
.put(node, value);
});
}
}
if (!collectionBuilders.isEmpty()) {
Map<Node, Object> nodeToCoreCount = metrics.computeIfAbsent(NodeMetricImpl.NUM_CORES, n -> new HashMap<>());
collectionBuilders.forEach(builder -> {
collectionMetrics.put(builder.collectionName, builder.collectionMetricsBuilder.build());
SolrCollection collection = builder.build();
collection.iterator().forEachRemaining(shard ->
shard.iterator().forEachRemaining(replica -> {
nodeToCoreCount.compute(replica.getNode(), (node, count) ->
(count == null) ? 1 : ((Number) count).intValue() + 1);
}));
});
}
AttributeValues attributeValues = new AttributeValuesImpl(sysprops, metrics, collectionMetrics);
return new AttributeFetcherForTest(attributeValues);
}
}
public static class CollectionBuilder {
private final String collectionName;
private LinkedList<ShardBuilder> shardBuilders = new LinkedList<>();
private Map<String, String> customProperties = new HashMap<>();
int replicaNumber = 0; // global replica numbering for the collection
private CollectionMetricsBuilder collectionMetricsBuilder = new CollectionMetricsBuilder();
public CollectionBuilder(String collectionName) {
this.collectionName = collectionName;
}
public CollectionBuilder addCustomProperty(String name, String value) {
customProperties.put(name, value);
return this;
}
public CollectionMetricsBuilder getCollectionMetricsBuilder() {
return collectionMetricsBuilder;
}
/**
* @return The internal shards data structure to allow test code to modify the replica distribution to nodes.
*/
public LinkedList<ShardBuilder> getShardBuilders() {
return shardBuilders;
}
/**
* Initializes the collection to a specific shard and replica distribution passed in {@code shardsReplicas}.
* @param shardsReplicas A list of shard descriptions, describing the replicas of that shard.
* Replica description include the replica type and the node on which the replica should be placed.
* Everything is text to make it easy to design specific collections. For example the following value:
* <pre>{@code
* List.of(
* List.of("NRT 0", "TLOG 0", "NRT 3"), // shard 1
* List.of("NRT 1", "NRT 3", "TLOG 2")); // shard 2
* }</pre>
* Creates a placement that would distribute replicas to nodes (there must be at least 4 nodes)
* in the following way:
* <pre>{@code
* +--------------+----+----+----+----+
* | Node | 0 | 1 | 2 | 3 |
* +----------------------------------+
* | Shard 1: | | | | |
* | NRT | X | | | X |
* | TLOG | X | | | |
* +----------------------------------+
* | Shard 2: | | | | |
* | NRT | | X | | X |
* | TLOG | | | X | |
* +--------------+----+----+----+----+
* }</pre>
*/
public CollectionBuilder customCollectionSetup(List<List<String>> shardsReplicas, List<NodeBuilder> liveNodes) {
shardBuilders = new LinkedList<>();
int shardNumber = 1; // Shard numbering starts at 1
for (List<String> replicasOnNodes : shardsReplicas) {
String shardName = buildShardName(shardNumber++);
LinkedList<ReplicaBuilder> replicas = new LinkedList<>();
ReplicaBuilder leader = null;
for (String replicaNode : replicasOnNodes) {
// replicaNode is like "TLOG 2" meaning a TLOG replica should be placed on node 2
String[] splited = replicaNode.split("\\s+");
Assert.assertEquals(2, splited.length);
Replica.ReplicaType type = Replica.ReplicaType.valueOf(splited[0]);
final NodeBuilder node;
int nodeIndex = Integer.parseInt(splited[1]);
if (nodeIndex < liveNodes.size()) {
node = liveNodes.get(nodeIndex);
} else {
// The collection can have replicas on non live nodes. Let's create such a node here (that is not known to the
// cluster). There could be many non live nodes in the collection configuration, they will all reference new
// instances such as below of a node unknown to cluster, but all will have the same name (so will be equal if
// tested).
node = new NodeBuilder().setNodeName("NonLiveNode");
}
String replicaName = buildReplicaName(shardName, type);
ReplicaBuilder replicaBuilder = new ReplicaBuilder();
replicaBuilder.setReplicaName(replicaName).setCoreName(buildCoreName(replicaName)).setReplicaType(type)
.setReplicaState(Replica.ReplicaState.ACTIVE).setReplicaNode(node);
replicas.add(replicaBuilder);
// No way to specify which replica is the leader. Could be done by adding a "*" to the replica definition for example
// in the passed shardsReplicas but not implementing this until it is needed :)
if (leader == null && type != Replica.ReplicaType.PULL) {
leader = replicaBuilder;
}
}
ShardBuilder shardBuilder = new ShardBuilder();
shardBuilder.setShardName(shardName).setReplicaBuilders(replicas).setLeader(leader);
shardBuilders.add(shardBuilder);
}
return this;
}
/**
* Initializes shard and replica builders for the collection based on passed parameters. Replicas are assigned round
* robin to the nodes. The shard leader is the first NRT replica of each shard (or first TLOG is no NRT).
* Shard and replica configuration can be modified afterwards, the returned builder hierarchy is a convenient starting point.
* @param countShards number of shards to create
* @param countNrtReplicas number of NRT replicas per shard
* @param countTlogReplicas number of TLOG replicas per shard
* @param countPullReplicas number of PULL replicas per shard
* @param nodes list of nodes to place replicas on.
*/
public CollectionBuilder initializeShardsReplicas(int countShards, int countNrtReplicas, int countTlogReplicas,
int countPullReplicas, List<NodeBuilder> nodes) {
return initializeShardsReplicas(countShards, countNrtReplicas, countTlogReplicas, countPullReplicas, nodes, null);
}
/**
* Initializes shard and replica builders for the collection based on passed parameters. Replicas are assigned round
* robin to the nodes. The shard leader is the first NRT replica of each shard (or first TLOG is no NRT).
* Shard and replica configuration can be modified afterwards, the returned builder hierarchy is a convenient starting point.
* @param countShards number of shards to create
* @param countNrtReplicas number of NRT replicas per shard
* @param countTlogReplicas number of TLOG replicas per shard
* @param countPullReplicas number of PULL replicas per shard
* @param nodes list of nodes to place replicas on.
* @param initialSizeGBPerShard initial replica size (in GB) per shard
*/
public CollectionBuilder initializeShardsReplicas(int countShards, int countNrtReplicas, int countTlogReplicas,
int countPullReplicas, List<NodeBuilder> nodes,
List<Integer> initialSizeGBPerShard) {
Iterator<NodeBuilder> nodeIterator = nodes.iterator();
shardBuilders = new LinkedList<>();
if (initialSizeGBPerShard != null && initialSizeGBPerShard.size() != countShards) {
throw new RuntimeException("list of shard sizes must be the same length as the countShards!");
}
for (int shardNumber = 1; shardNumber <= countShards; shardNumber++) {
String shardName = buildShardName(shardNumber);
CollectionMetricsBuilder.ShardMetricsBuilder shardMetricsBuilder = new CollectionMetricsBuilder.ShardMetricsBuilder(shardName);
LinkedList<ReplicaBuilder> replicas = new LinkedList<>();
ReplicaBuilder leader = null;
CollectionMetricsBuilder.ReplicaMetricsBuilder leaderMetrics = null;
// Iterate on requested counts, NRT then TLOG then PULL. Leader chosen as first NRT (or first TLOG if no NRT)
List<Pair<Replica.ReplicaType, Integer>> replicaTypes = List.of(
new Pair<>(Replica.ReplicaType.NRT, countNrtReplicas),
new Pair<>(Replica.ReplicaType.TLOG, countTlogReplicas),
new Pair<>(Replica.ReplicaType.PULL, countPullReplicas));
for (Pair<Replica.ReplicaType, Integer> tc : replicaTypes) {
Replica.ReplicaType type = tc.first();
int count = tc.second();
for (int r = 0; r < count; r++) {
if (!nodeIterator.hasNext()) {
nodeIterator = nodes.iterator();
}
// If the nodes set is empty, this call will fail
final NodeBuilder node = nodeIterator.next();
String replicaName = buildReplicaName(shardName, type);
ReplicaBuilder replicaBuilder = new ReplicaBuilder();
replicaBuilder.setReplicaName(replicaName).setCoreName(buildCoreName(replicaName)).setReplicaType(type)
.setReplicaState(Replica.ReplicaState.ACTIVE).setReplicaNode(node);
replicas.add(replicaBuilder);
CollectionMetricsBuilder.ReplicaMetricsBuilder replicaMetricsBuilder = new CollectionMetricsBuilder.ReplicaMetricsBuilder(replicaName);
shardMetricsBuilder.getReplicaMetricsBuilders().put(replicaName, replicaMetricsBuilder);
if (initialSizeGBPerShard != null) {
replicaMetricsBuilder.addMetric(ReplicaMetricImpl.INDEX_SIZE_GB, initialSizeGBPerShard.get(shardNumber - 1) * ReplicaMetricImpl.GB);
}
if (leader == null && type != Replica.ReplicaType.PULL) {
leader = replicaBuilder;
leaderMetrics = replicaMetricsBuilder;
}
}
}
ShardBuilder shardBuilder = new ShardBuilder();
shardBuilder.setShardName(shardName).setReplicaBuilders(replicas).setLeader(leader);
shardMetricsBuilder.setLeaderMetrics(leaderMetrics);
shardBuilders.add(shardBuilder);
collectionMetricsBuilder.getShardMetricsBuilders().put(shardName, shardMetricsBuilder);
}
return this;
}
private String buildShardName(int shardIndex) {
return "shard" + shardIndex;
}
private String buildReplicaName(String shardName, Replica.ReplicaType replicaType) {
return collectionName + "_" + shardName + "_replica_" + replicaType.getSuffixChar() + replicaNumber++;
}
private String buildCoreName(String replicaName) {
return replicaName + "_c";
}
public SolrCollection build() {
ClusterAbstractionsForTest.SolrCollectionImpl solrCollection = new ClusterAbstractionsForTest.SolrCollectionImpl(collectionName, customProperties);
final LinkedHashMap<String, Shard> shards = new LinkedHashMap<>();
for (ShardBuilder shardBuilder : shardBuilders) {
Shard shard = shardBuilder.build(solrCollection);
shards.put(shard.getShardName(), shard);
}
solrCollection.setShards(shards);
return solrCollection;
}
}
public static class ShardBuilder {
private String shardName;
private LinkedList<ReplicaBuilder> replicaBuilders = new LinkedList<>();
private ReplicaBuilder leaderReplicaBuilder;
public ShardBuilder setShardName(String shardName) {
this.shardName = shardName;
return this;
}
public String getShardName() {
return shardName;
}
public LinkedList<ReplicaBuilder> getReplicaBuilders() {
return replicaBuilders;
}
public ShardBuilder setReplicaBuilders(LinkedList<ReplicaBuilder> replicaBuilders) {
this.replicaBuilders = replicaBuilders;
return this;
}
public ShardBuilder setLeader(ReplicaBuilder leaderReplicaBuilder) {
this.leaderReplicaBuilder = leaderReplicaBuilder;
return this;
}
public Shard build(SolrCollection collection) {
ClusterAbstractionsForTest.ShardImpl shard = new ClusterAbstractionsForTest.ShardImpl(shardName, collection, Shard.ShardState.ACTIVE);
final LinkedHashMap<String, Replica> replicas = new LinkedHashMap<>();
Replica leader = null;
for (ReplicaBuilder replicaBuilder : replicaBuilders) {
Replica replica = replicaBuilder.build(shard);
replicas.put(replica.getReplicaName(), replica);
if (leaderReplicaBuilder == replicaBuilder) {
leader = replica;
}
}
shard.setReplicas(replicas, leader);
return shard;
}
}
public static class ReplicaBuilder {
private String replicaName;
private String coreName;
private Replica.ReplicaType replicaType;
private Replica.ReplicaState replicaState;
private NodeBuilder replicaNode;
private Map<ReplicaMetric<?>, Object> metrics;
public ReplicaBuilder setReplicaName(String replicaName) {
this.replicaName = replicaName;
return this;
}
public ReplicaBuilder setCoreName(String coreName) {
this.coreName = coreName;
return this;
}
public Replica.ReplicaType getReplicaType() {
return replicaType;
}
public ReplicaBuilder setReplicaType(Replica.ReplicaType replicaType) {
this.replicaType = replicaType;
return this;
}
public ReplicaBuilder setReplicaState(Replica.ReplicaState replicaState) {
this.replicaState = replicaState;
return this;
}
public ReplicaBuilder setReplicaNode(NodeBuilder replicaNode) {
this.replicaNode = replicaNode;
return this;
}
public ReplicaBuilder setReplicaMetric(ReplicaMetric<?> metric, Object value) {
if (metrics == null) {
metrics = new HashMap<>();
}
metrics.put(metric, metric.convert(value));
return this;
}
public Replica build(Shard shard) {
return new ClusterAbstractionsForTest.ReplicaImpl(replicaName, coreName, shard, replicaType, replicaState, replicaNode.build());
}
}
public static class NodeBuilder {
private String nodeName = null;
private Integer coreCount = null;
private Double freeDiskGB = null;
private Double totalDiskGB = null;
private Map<String, String> sysprops = null;
private Map<NodeMetric<?>, Object> metrics = null;
public NodeBuilder setNodeName(String nodeName) {
this.nodeName = nodeName;
return this;
}
public NodeBuilder setCoreCount(Integer coreCount) {
this.coreCount = coreCount;
return this;
}
public NodeBuilder setFreeDiskGB(Double freeDiskGB) {
this.freeDiskGB = freeDiskGB;
return this;
}
public NodeBuilder setTotalDiskGB(Double totalDiskGB) {
this.totalDiskGB = totalDiskGB;
return this;
}
public NodeBuilder setSysprop(String key, String value) {
if (sysprops == null) {
sysprops = new HashMap<>();
}
String name = AttributeFetcherImpl.getSystemPropertySnitchTag(key);
sysprops.put(name, value);
return this;
}
public NodeBuilder setMetric(NodeMetric<?> metric, Object value) {
if (metrics == null) {
metrics = new HashMap<>();
}
metrics.put(metric, metric.convert(value));
return this;
}
public Integer getCoreCount() {
return coreCount;
}
public Double getFreeDiskGB() {
return freeDiskGB;
}
public Double getTotalDiskGB() {
return totalDiskGB;
}
public Map<String, String> getSysprops() {
return sysprops;
}
public Map<NodeMetric<?>, Object> getMetrics() {
return metrics;
}
public Node build() {
// It is ok to build a new instance each time, that instance does the right thing with equals() and hashCode()
return new ClusterAbstractionsForTest.NodeImpl(nodeName);
}
}
}