SOLR-11985: Support percentage values in replica attribute in autoscaling policy

SOLR-12511: Support non integer values for replica in autoscaling policy

SOLR-12517: Support range values for replica in autoscaling policy
This commit is contained in:
Noble Paul 2018-06-26 16:28:17 +10:00
parent 095f9eb90d
commit 1eb2676f27
14 changed files with 1040 additions and 398 deletions

View File

@ -90,6 +90,12 @@ New Features
* SOLR-12398: The JSON Facet API now supports type=heatmap facets, just as classic faceting does. (David Smiley)
* SOLR-11985: Support percentage values in replica attribute in autoscaling policy (noble)
* SOLR-12511: Support non integer values for replica in autoscaling policy (noble)
* SOLR-12517: Support range values for replica in autoscaling policy (noble)
Bug Fixes
----------------------

View File

@ -36,13 +36,13 @@ import java.util.stream.Stream;
import org.apache.solr.api.Api;
import org.apache.solr.api.ApiBag;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.cloud.autoscaling.AutoScalingConfig;
import org.apache.solr.client.solrj.cloud.autoscaling.BadVersionException;
import org.apache.solr.client.solrj.cloud.autoscaling.Clause;
import org.apache.solr.client.solrj.cloud.autoscaling.Policy;
import org.apache.solr.client.solrj.cloud.autoscaling.PolicyHelper;
import org.apache.solr.client.solrj.cloud.autoscaling.Preference;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.cloud.autoscaling.TriggerEventProcessorStage;
import org.apache.solr.client.solrj.cloud.autoscaling.TriggerEventType;
import org.apache.solr.common.MapWriter;
@ -52,6 +52,7 @@ import org.apache.solr.common.params.CollectionAdminParams;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.TimeSource;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.handler.RequestHandlerBase;
@ -61,7 +62,6 @@ import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.common.util.TimeSource;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -252,7 +252,7 @@ public class AutoScalingHandler extends RequestHandlerBase implements Permission
}
List<Clause> cp = null;
try {
cp = clusterPolicy.stream().map(Clause::new).collect(Collectors.toList());
cp = clusterPolicy.stream().map(Clause::create).collect(Collectors.toList());
} catch (Exception e) {
op.addError(e.getMessage());
return currentConfig;

View File

@ -831,7 +831,7 @@ public class AutoScalingHandlerTest extends SolrCloudTestCase {
for (Map<String, Object> violation : violations) {
assertEquals("readApiTestViolations", violation.get("collection"));
assertEquals("shard1", violation.get("shard"));
assertEquals(2l, getObjectByPath(violation, true, "violation/delta"));
assertEquals(2d, getObjectByPath(violation, true, "violation/delta"));
assertEquals(3l, getObjectByPath(violation, true, "violation/replica/NRT"));
assertNotNull(violation.get("clause"));
}
@ -842,7 +842,7 @@ public class AutoScalingHandlerTest extends SolrCloudTestCase {
response = solrClient.request(req);
List l = (List) response.get("suggestions");
assertNotNull(l);
assertEquals(1, l.size());
assertEquals(2, l.size());
for (int i = 0; i < l.size(); i++) {
Object suggestion = l.get(i);
assertEquals("violation", Utils.getObjectByPath(suggestion, true, "type"));

View File

@ -44,14 +44,15 @@ class AddReplicaSuggester extends Suggester {
}
for (Pair<String, String> shard : shards) {
Replica.Type type = Replica.Type.get((String) hints.get(Hint.REPLICATYPE));
//iterate through elemenodesnts and identify the least loaded
//iterate through nodes and identify the least loaded
List<Violation> leastSeriousViolation = null;
Row bestNode = null;
for (int i = getMatrix().size() - 1; i >= 0; i--) {
Row row = getMatrix().get(i);
if (!isNodeSuitableForReplicaAddition(row)) continue;
Row tmpRow = row.addReplica(shard.first(), shard.second(), type);
List<Violation> errs = testChangedMatrix(strict, tmpRow.session.matrix);
List<Violation> errs = testChangedMatrix(strict, tmpRow.session);
if (!containsNewErrors(errs)) {
if (isLessSerious(errs, leastSeriousViolation)) {
leastSeriousViolation = errs;

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.cloud.Replica;
@ -52,13 +53,25 @@ import static org.apache.solr.common.params.CoreAdminParams.SHARD;
public class Clause implements MapWriter, Comparable<Clause> {
private static final Set<String> IGNORE_TAGS = new HashSet<>(Arrays.asList(REPLICA, COLLECTION, SHARD, "strict", "type"));
final boolean hasComputedValue;
final Map<String, Object> original;
Condition collection, shard, replica, tag, globalTag;
final Replica.Type type;
boolean strict;
public Clause(Map<String, Object> m) {
protected Clause(Clause clause, Function<Condition, Object> computedValueEvaluator) {
this.original = clause.original;
this.type = clause.type;
this.collection = clause.collection;
this.shard = clause.shard;
this.tag = evaluateValue(clause.tag, computedValueEvaluator);
this.replica = evaluateValue(clause.replica, computedValueEvaluator);
this.globalTag = evaluateValue(clause.globalTag, computedValueEvaluator);
this.hasComputedValue = clause.hasComputedValue;
this.strict = clause.strict;
}
private Clause(Map<String, Object> m) {
this.original = Utils.getDeepCopy(m, 10);
String type = (String) m.get("type");
this.type = type == null || ANY.equals(type) ? null : Replica.Type.valueOf(type.toUpperCase(Locale.ROOT));
@ -90,7 +103,47 @@ public class Clause implements MapWriter, Comparable<Clause> {
throw new RuntimeException("Invalid metrics: param in " + Utils.toJSONString(m) + " must have at 2 or 3 segments after 'metrics:' separated by ':'");
}
}
hasComputedValue = hasComputedValue();
}
public static Clause create(Map<String, Object> m) {
Clause clause = new Clause(m);
return clause.hasComputedValue() ?
clause :
clause.getSealedClause(null);
}
public static String parseString(Object val) {
if (val instanceof Condition) val = ((Condition) val).val;
return val == null ? null : String.valueOf(val);
}
public Condition getCollection() {
return collection;
}
public Condition getShard() {
return shard;
}
public Condition getReplica() {
return replica;
}
public Condition getTag() {
return tag;
}
public Condition getGlobalTag() {
return globalTag;
}
private Condition evaluateValue(Condition condition, Function<Condition, Object> computedValueEvaluator) {
if (condition == null) return null;
if (condition.computationType == null) return condition;
Object val = computedValueEvaluator.apply(condition);
val = condition.op.readRuleValue(new Condition(condition.name, val, condition.op, null, null));
return new Condition(condition.name, val, condition.op, null, this);
}
public boolean doesOverride(Clause that) {
@ -118,6 +171,14 @@ public class Clause implements MapWriter, Comparable<Clause> {
return 0;
}
private boolean hasComputedValue() {
if (replica != null && replica.computationType != null) return true;
if (tag != null && tag.computationType != null) return true;
if (globalTag != null && globalTag.computationType != null) return true;
return false;
}
@Override
public int compareTo(Clause that) {
int v = Integer.compare(this.tag.op.priority, that.tag.op.priority);
@ -125,7 +186,7 @@ public class Clause implements MapWriter, Comparable<Clause> {
if (this.isPerCollectiontag() && that.isPerCollectiontag()) {
v = Integer.compare(this.replica.op.priority, that.replica.op.priority);
if (v == 0) {// higher the number of replicas , harder to satisfy
v = Long.compare((Long) this.replica.val, (Long) that.replica.val);
v = Preference.compareWithTolerance((Double) this.replica.val, (Double) that.replica.val, 1);
v = this.replica.op == LESS_THAN ? v : v * -1;
}
if (v == 0) v = compareTypes(this.type, that.type);
@ -135,39 +196,217 @@ public class Clause implements MapWriter, Comparable<Clause> {
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Clause that = (Clause)o;
return Objects.equals(this.original, that.original);
}
void addTags(Collection<String> params) {
if (globalTag != null && !params.contains(globalTag.name)) params.add(globalTag.name);
if (tag != null && !params.contains(tag.name)) params.add(tag.name);
}
boolean isReplicaZero() {
return replica != null && replica.getOperand() == EQUAL && 0L == (Long) replica.val;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Clause)) return false;
Clause that = (Clause)o;
return Objects.equals(this.original, that.original);
}
class Condition {
//replica value is zero
boolean isReplicaZero() {
return replica != null && replica.getOperand() == EQUAL &&
Preference.compareWithTolerance(0d, (Double) replica.val, 1) == 0;
}
public SealedClause getSealedClause(Function<Condition, Object> computedValueEvaluator) {
return this instanceof SealedClause ?
(SealedClause) this :
new SealedClause(this, computedValueEvaluator);
}
Condition parse(String s, Map m) {
Object expectedVal = null;
ComputationType computationType = null;
Object val = m.get(s);
Suggestion.ConditionType varType = Suggestion.getTagType(s);
try {
String conditionName = s.trim();
Operand operand = null;
if (val == null) {
operand = WILDCARD;
expectedVal = Policy.ANY;
} else if (val instanceof String) {
String strVal = ((String) val).trim();
if (Policy.ANY.equals(strVal) || Policy.EACH.equals(strVal)) operand = WILDCARD;
else if (strVal.startsWith(NOT_EQUAL.operand)) operand = NOT_EQUAL;
else if (strVal.startsWith(GREATER_THAN.operand)) operand = GREATER_THAN;
else if (strVal.startsWith(LESS_THAN.operand)) operand = LESS_THAN;
else operand = EQUAL;
strVal = strVal.substring(EQUAL == operand || WILDCARD == operand ? 0 : 1);
for (ComputationType t : ComputationType.values()) {
String changedVal = t.match(strVal);
if (changedVal != null) {
computationType = t;
strVal = changedVal;
if (varType == null || !varType.supportComputed(computationType)) {
throw new IllegalArgumentException(StrUtils.formatString("''{0}'' is not allowed for variable : ''{1}'' , in condition : ''{2}'' ",
t, conditionName, Utils.toJSONString(m)));
}
}
}
operand = varType == null ? operand : varType.getOperand(operand, strVal, computationType);
expectedVal = validate(s, new Condition(s, strVal, operand, computationType, null), true);
} else if (val instanceof Number) {
operand = EQUAL;
operand = varType.getOperand(operand, val, null);
expectedVal = validate(s, new Condition(s, val, operand, null, null), true);
}
return new Condition(conditionName, expectedVal, operand, computationType, this);
} catch (IllegalArgumentException iae) {
throw iae;
} catch (Exception e) {
throw new IllegalArgumentException("Invalid tag : " + s + ":" + val, e);
}
}
public List<Violation> test(Policy.Session session) {
ComputedValueEvaluator computedValueEvaluator = new ComputedValueEvaluator(session);
Suggestion.ViolationCtx ctx = new Suggestion.ViolationCtx(this, session.matrix, computedValueEvaluator);
if (isPerCollectiontag()) {
Map<String, Map<String, Map<String, ReplicaCount>>> replicaCount = computeReplicaCounts(session.matrix, computedValueEvaluator);
for (Map.Entry<String, Map<String, Map<String, ReplicaCount>>> e : replicaCount.entrySet()) {
computedValueEvaluator.collName = e.getKey();
if (!collection.isPass(computedValueEvaluator.collName)) continue;
for (Map.Entry<String, Map<String, ReplicaCount>> shardVsCount : e.getValue().entrySet()) {
computedValueEvaluator.shardName = shardVsCount.getKey();
if (!shard.isPass(computedValueEvaluator.shardName)) continue;
for (Map.Entry<String, ReplicaCount> counts : shardVsCount.getValue().entrySet()) {
SealedClause sealedClause = getSealedClause(computedValueEvaluator);
if (!sealedClause.replica.isPass(counts.getValue())) {
Violation violation = new Violation(sealedClause,
computedValueEvaluator.collName,
computedValueEvaluator.shardName,
tag.name.equals("node") ? counts.getKey() : null,
counts.getValue(),
sealedClause.getReplica().delta(counts.getValue()),
counts.getKey());
Suggestion.getTagType(tag.name).addViolatingReplicas(ctx.reset(counts.getKey(), counts.getValue(), violation));
}
}
}
}
} else {
for (Row r : session.matrix) {
SealedClause sealedClause = getSealedClause(computedValueEvaluator);
if (!sealedClause.getGlobalTag().isPass(r)) {
Suggestion.ConditionType.CORES.addViolatingReplicas(ctx.reset(null, null,
new Violation(sealedClause, null, null, r.node, r.getVal(sealedClause.globalTag.name), sealedClause.globalTag.delta(r.getVal(globalTag.name)), null)));
}
}
}
return ctx.allViolations;
}
private Map<String, Map<String, Map<String, ReplicaCount>>> computeReplicaCounts(List<Row> allRows,
ComputedValueEvaluator computedValueEvaluator) {
Map<String, Map<String, Map<String, ReplicaCount>>> collVsShardVsTagVsCount = new HashMap<>();
for (Row row : allRows) {
for (Map.Entry<String, Map<String, List<ReplicaInfo>>> colls : row.collectionVsShardVsReplicas.entrySet()) {
String collectionName = colls.getKey();
if (!collection.isPass(collectionName)) continue;
Map<String, Map<String, ReplicaCount>> collMap = collVsShardVsTagVsCount.computeIfAbsent(collectionName, s -> new HashMap<>());
for (Map.Entry<String, List<ReplicaInfo>> shards : colls.getValue().entrySet()) {
String shardName = shards.getKey();
if (ANY.equals(shard.val)) shardName = ANY;
if (!shard.isPass(shardName)) break;
Map<String, ReplicaCount> tagVsCount = collMap.computeIfAbsent(shardName, s -> new HashMap<>());
Object tagVal = row.getVal(tag.name);
computedValueEvaluator.collName = collectionName;
computedValueEvaluator.shardName = shardName;
SealedClause sealedClause = getSealedClause(computedValueEvaluator);
boolean pass = sealedClause.getTag().isPass(tagVal);
tagVsCount.computeIfAbsent(pass ? String.valueOf(tagVal) : "", s -> new ReplicaCount());
if (pass) {
tagVsCount.get(String.valueOf(tagVal)).increment(shards.getValue());
}
}
}
}
return collVsShardVsTagVsCount;
}
enum ComputationType {
PERCENT {
@Override
public String wrap(String value) {
return value + "%";
}
@Override
public String match(String val) {
if (val != null && !val.isEmpty() && val.charAt(val.length() - 1) == '%') {
return val.substring(0, val.length() - 1);
} else {
return null;
}
}
@Override
public String toString() {
return "%";
}
};
// return null if there is no match. return a modified string
// if there is a match
public String match(String val) {
return null;
}
public String wrap(String value) {
return value;
}
}
public static class Condition implements MapWriter {
final String name;
final Object val;
final Suggestion.ConditionType varType;
final ComputationType computationType;
final Operand op;
private Clause clause;
Condition(String name, Object val, Operand op) {
Condition(String name, Object val, Operand op, ComputationType computationType, Clause parent) {
this.name = name;
this.val = val;
this.op = op;
varType = Suggestion.getTagType(name);
this.computationType = computationType;
this.clause = parent;
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
String value = op.wrap(val);
if (computationType != null) value = computationType.wrap(value);
ew.put(name, value);
}
@Override
public String toString() {
return jsonStr();
}
public Clause getClause() {
return clause;
}
boolean isPass(Object inputVal) {
if (inputVal instanceof ReplicaCount) inputVal = ((ReplicaCount) inputVal).getVal(type);
if (computationType != null) {
throw new IllegalStateException("This is supposed to be called only from a Condition with no computed value or a SealedCondition");
}
if (inputVal instanceof ReplicaCount) inputVal = ((ReplicaCount) inputVal).getVal(getClause().type);
if (varType == Suggestion.ConditionType.LAZY) { // we don't know the type
return op.match(parseString(val), parseString(inputVal)) == PASS;
} else {
@ -177,7 +416,7 @@ public class Clause implements MapWriter, Comparable<Clause> {
boolean isPass(Row row) {
return op.match(val, row.getVal(name)) == PASS;
return isPass(row.getVal(name));
}
@Override
@ -189,18 +428,18 @@ public class Clause implements MapWriter, Comparable<Clause> {
return false;
}
public Long delta(Object val) {
if (val instanceof ReplicaCount) val = ((ReplicaCount) val).getVal(type);
public Double delta(Object val) {
if (val instanceof ReplicaCount) val = ((ReplicaCount) val).getVal(getClause().type);
if (this.val instanceof String) {
if (op == LESS_THAN || op == GREATER_THAN) {
return op
.opposite(isReplicaZero() && this == tag)
.opposite(getClause().isReplicaZero() && this == getClause().tag)
.delta(Clause.parseDouble(name, this.val), Clause.parseDouble(name, val));
} else {
return 0L;
return 0d;
}
} else return op
.opposite(isReplicaZero() && this == tag)
.opposite(getClause().isReplicaZero() && this == getClause().getTag())
.delta(this.val, val);
}
@ -217,94 +456,6 @@ public class Clause implements MapWriter, Comparable<Clause> {
}
}
Condition parse(String s, Map m) {
Object expectedVal = null;
Object val = m.get(s);
try {
String conditionName = s.trim();
Operand operand = null;
if (val == null) {
operand = WILDCARD;
expectedVal = Policy.ANY;
} else if (val instanceof String) {
String strVal = ((String) val).trim();
if (Policy.ANY.equals(strVal) || Policy.EACH.equals(strVal)) operand = WILDCARD;
else if (strVal.startsWith(NOT_EQUAL.operand)) operand = NOT_EQUAL;
else if (strVal.startsWith(GREATER_THAN.operand)) operand = GREATER_THAN;
else if (strVal.startsWith(LESS_THAN.operand)) operand = LESS_THAN;
else operand = EQUAL;
expectedVal = validate(s, strVal.substring(EQUAL == operand || WILDCARD == operand ? 0 : 1), true);
} else if (val instanceof Number) {
operand = EQUAL;
expectedVal = validate(s, val, true);
}
return new Condition(conditionName, expectedVal, operand);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid tag : " + s + ":" + val, e);
}
}
public List<Violation> test(List<Row> allRows) {
Suggestion.ViolationCtx ctx = new Suggestion.ViolationCtx(this, allRows);
if (isPerCollectiontag()) {
Map<String, Map<String, Map<String, ReplicaCount>>> replicaCount = computeReplicaCounts(allRows);
for (Map.Entry<String, Map<String, Map<String, ReplicaCount>>> e : replicaCount.entrySet()) {
if (!collection.isPass(e.getKey())) continue;
for (Map.Entry<String, Map<String, ReplicaCount>> shardVsCount : e.getValue().entrySet()) {
if (!shard.isPass(shardVsCount.getKey())) continue;
for (Map.Entry<String, ReplicaCount> counts : shardVsCount.getValue().entrySet()) {
if (!replica.isPass(counts.getValue())) {
Violation violation = new Violation(this,
e.getKey(),
shardVsCount.getKey(),
tag.name.equals("node") ? counts.getKey() : null,
counts.getValue(),
replica.delta(counts.getValue()),
counts.getKey());
Suggestion.getTagType(tag.name).addViolatingReplicas(ctx.reset(counts.getKey(), counts.getValue(), violation));
}
}
}
}
} else {
for (Row r : allRows) {
if (!globalTag.isPass(r)) {
Suggestion.ConditionType.CORES.addViolatingReplicas(ctx.reset(null, null,
new Violation(this, null, null, r.node, r.getVal(globalTag.name), globalTag.delta(r.getVal(globalTag.name)), null)));
}
}
}
return ctx.allViolations;
}
private Map<String, Map<String, Map<String, ReplicaCount>>> computeReplicaCounts(List<Row> allRows) {
Map<String, Map<String, Map<String, ReplicaCount>>> collVsShardVsTagVsCount = new HashMap<>();
for (Row row : allRows) {
for (Map.Entry<String, Map<String, List<ReplicaInfo>>> colls : row.collectionVsShardVsReplicas.entrySet()) {
String collectionName = colls.getKey();
if (!collection.isPass(collectionName)) continue;
Map<String, Map<String, ReplicaCount>> collMap = collVsShardVsTagVsCount.computeIfAbsent(collectionName, s -> new HashMap<>());
for (Map.Entry<String, List<ReplicaInfo>> shards : colls.getValue().entrySet()) {
String shardName = shards.getKey();
if (ANY.equals(shard.val)) shardName = ANY;
if (!shard.isPass(shardName)) break;
Map<String, ReplicaCount> tagVsCount = collMap.computeIfAbsent(shardName, s -> new HashMap<>());
Object tagVal = row.getVal(tag.name);
boolean pass = tag.isPass(tagVal);
tagVsCount.computeIfAbsent(pass ? String.valueOf(tagVal) : "", s -> new ReplicaCount());
if (pass) {
tagVsCount.get(String.valueOf(tagVal)).increment(shards.getValue());
}
}
}
}
return collVsShardVsTagVsCount;
}
public boolean isStrict() {
return strict;
}
@ -323,8 +474,20 @@ public class Clause implements MapWriter, Comparable<Clause> {
NOT_APPLICABLE, FAIL, PASS
}
public static String parseString(Object val) {
return val == null ? null : String.valueOf(val);
public static class ComputedValueEvaluator implements Function<Condition, Object> {
final Policy.Session session;
String collName = null;
String shardName = null;
public ComputedValueEvaluator(Policy.Session session) {
this.session = session;
}
@Override
public Object apply(Condition computedValue) {
return computedValue.varType.computeValue(session, computedValue, collName, shardName);
}
}
/**
@ -389,4 +552,31 @@ public class Clause implements MapWriter, Comparable<Clause> {
public static final String METRICS_PREFIX = "metrics:";
static class RangeVal implements MapWriter {
final Number min, max, actual;
RangeVal(Number min, Number max, Number actual) {
this.min = min;
this.max = max;
this.actual = actual;
}
public boolean match(Number testVal) {
return Double.compare(testVal.doubleValue(), min.doubleValue()) >= 0 &&
Double.compare(testVal.doubleValue(), max.doubleValue()) <= 0;
}
public Double delta(double v) {
if (actual != null) return v - actual.doubleValue();
if (v >= max.doubleValue()) return v - max.doubleValue();
if (v <= min.doubleValue()) return v - min.doubleValue();
return 0d;
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
ew.put("min", min).put("max", max).putIfNotNull("actual", actual);
}
}
}

View File

@ -60,7 +60,7 @@ public class MoveReplicaSuggester extends Suggester {
Pair<Row, ReplicaInfo> pair = targetRow.session.getNode(fromRow.node).removeReplica(ri.getCollection(), ri.getShard(), ri.getType());//then remove replica from source node
if (pair == null) continue;//should not happen
Row srcRowModified = pair.first();//this is the final state of the source row and session
List<Violation> errs = testChangedMatrix(strict, srcRowModified.session.matrix);
List<Violation> errs = testChangedMatrix(strict, srcRowModified.session);
srcRowModified.session.applyRules();// now resort the nodes with the new values
Policy.Session tmpSession = srcRowModified.session;
if (!containsNewErrors(errs) &&

View File

@ -35,12 +35,59 @@ public enum Operand {
}
},
RANGE_EQUAL("", 0) {
@Override
public TestStatus match(Object ruleVal, Object testVal) {
return ((Clause.RangeVal) ruleVal).match((Number) testVal) ? PASS : FAIL;
}
@Override
public Double delta(Object expected, Object actual) {
return ((Clause.RangeVal) expected).delta(((Number) actual).doubleValue());
}
@Override
public Object readRuleValue(Clause.Condition condition) {
if (condition.val instanceof String) {
String strVal = ((String) condition.val).trim();
int hyphenIdx = strVal.indexOf('-');
if (hyphenIdx > 0) {
String minS = strVal.substring(0, hyphenIdx).trim();
String maxS = strVal.substring(hyphenIdx + 1, strVal.length()).trim();
return new Clause.RangeVal(
(Number) condition.varType.validate(condition.name, minS, true),
(Number) condition.varType.validate(condition.name, maxS, true),
null
);
}
}
Number num = (Number) condition.varType.validate(condition.name, condition.val, true);
return new Clause.RangeVal(Math.floor(num.doubleValue()), Math.ceil(num.doubleValue()), num);
}
},
EQUAL("", 0) {
@Override
public long _delta(long expected, long actual) {
public double _delta(double expected, double actual) {
return expected - actual;
}
},
RANGE_NOT_EQUAL("", 2) {
@Override
public TestStatus match(Object ruleVal, Object testVal) {
return ((Clause.RangeVal) ruleVal).match((Number) testVal) ? FAIL : PASS;
}
@Override
public Object readRuleValue(Clause.Condition condition) {
return RANGE_EQUAL.readRuleValue(condition);
}
},
NOT_EQUAL("!", 2) {
@Override
public TestStatus match(Object ruleVal, Object testVal) {
@ -48,7 +95,7 @@ public enum Operand {
}
@Override
public long _delta(long expected, long actual) {
public double _delta(double expected, double actual) {
return expected - actual;
}
@ -59,19 +106,24 @@ public enum Operand {
if (testVal == null) return NOT_APPLICABLE;
if (ruleVal instanceof String) ruleVal = Clause.parseDouble("", ruleVal);
if (ruleVal instanceof Double) {
return Double.compare(Clause.parseDouble("", testVal), (Double) ruleVal) == 1 ? PASS : FAIL;
return Double.compare(Clause.parseDouble("", testVal), (Double) ruleVal) == -1 ? FAIL : PASS;
}
return getLong(testVal) > getLong(ruleVal) ? PASS: FAIL ;
}
@Override
public String wrap(Object val) {
return ">" + (((Number) val).doubleValue() - 1);
}
@Override
public Operand opposite(boolean flag) {
return flag ? LESS_THAN : GREATER_THAN;
}
@Override
protected long _delta(long expected, long actual) {
return actual > expected ? 0 : (expected + 1) - actual;
protected double _delta(double expected, double actual) {
return actual > expected ? 0 : expected - actual;
}
},
LESS_THAN("<", 2) {
@ -80,14 +132,19 @@ public enum Operand {
if (testVal == null) return NOT_APPLICABLE;
if (ruleVal instanceof String) ruleVal = Clause.parseDouble("", ruleVal);
if (ruleVal instanceof Double) {
return Double.compare(Clause.parseDouble("", testVal), (Double) ruleVal) == -1 ? PASS : FAIL;
return Double.compare(Clause.parseDouble("", testVal), (Double) ruleVal) == 1 ? FAIL : PASS;
}
return getLong(testVal) < getLong(ruleVal) ? PASS: FAIL ;
}
@Override
protected long _delta(long expected, long actual) {
return actual < expected ? 0 : (actual + 1) - expected;
public String wrap(Object val) {
return "<" + (((Number) val).doubleValue() + 1);
}
@Override
protected double _delta(double expected, double actual) {
return actual < expected ? 0 : actual - expected;
}
@Override
@ -118,18 +175,26 @@ public enum Operand {
}
public Long delta(Object expected, Object actual) {
public Double delta(Object expected, Object actual) {
if (expected instanceof Number && actual instanceof Number) {
Long expectedL = ((Number) expected).longValue();
Long actualL = ((Number) actual).longValue();
Double expectedL = ((Number) expected).doubleValue();
Double actualL = ((Number) actual).doubleValue();
return _delta(expectedL, actualL);
} else {
return 0L;
return 0d;
}
}
protected long _delta(long expected, long actual) {
protected double _delta(double expected, double actual) {
return 0;
}
public String wrap(Object val) {
return operand + val.toString();
}
public Object readRuleValue(Clause.Condition condition) {
return condition.val;
}
}

View File

@ -112,7 +112,7 @@ public class Policy implements MapWriter {
clusterPreferences.forEach(preference -> paramsOfInterest.add(preference.name.toString()));
List<String> newParams = new ArrayList<>(paramsOfInterest);
clusterPolicy = ((List<Map<String, Object>>) jsonMap.getOrDefault(CLUSTER_POLICY, emptyList())).stream()
.map(Clause::new)
.map(Clause::create)
.filter(clause -> {
clause.addTags(newParams);
return true;
@ -219,144 +219,20 @@ public class Policy implements MapWriter {
return getClusterPreferences().equals(policy.getClusterPreferences());
}
/*This stores the logical state of the system, given a policy and
* a cluster state.
*
*/
public class Session implements MapWriter {
final List<String> nodes;
final SolrCloudManager cloudManager;
final List<Row> matrix;
Set<String> collections = new HashSet<>();
List<Clause> expandedClauses;
List<Violation> violations = new ArrayList<>();
final NodeStateProvider nodeStateProvider;
final int znodeVersion;
private Session(List<String> nodes, SolrCloudManager cloudManager,
List<Row> matrix, List<Clause> expandedClauses, int znodeVersion, NodeStateProvider nodeStateProvider) {
this.nodes = nodes;
this.cloudManager = cloudManager;
this.matrix = matrix;
this.expandedClauses = expandedClauses;
this.znodeVersion = znodeVersion;
this.nodeStateProvider = nodeStateProvider;
for (Row row : matrix) row.session = this;
}
Session(SolrCloudManager cloudManager) {
ClusterState state = null;
this.nodeStateProvider = cloudManager.getNodeStateProvider();
try {
state = cloudManager.getClusterStateProvider().getClusterState();
LOG.trace("-- session created with cluster state: {}", state);
} catch (Exception e) {
LOG.trace("-- session created, can't obtain cluster state", e);
}
this.znodeVersion = state != null ? state.getZNodeVersion() : -1;
this.nodes = new ArrayList<>(cloudManager.getClusterStateProvider().getLiveNodes());
this.cloudManager = cloudManager;
for (String node : nodes) {
collections.addAll(nodeStateProvider.getReplicaInfo(node, Collections.emptyList()).keySet());
}
expandedClauses = clusterPolicy.stream()
.filter(clause -> !clause.isPerCollectiontag())
.collect(Collectors.toList());
ClusterStateProvider stateProvider = cloudManager.getClusterStateProvider();
for (String c : collections) {
addClausesForCollection(stateProvider, c);
}
Collections.sort(expandedClauses);
matrix = new ArrayList<>(nodes.size());
for (String node : nodes) matrix.add(new Row(node, params, perReplicaAttributes, this));
applyRules();
}
void addClausesForCollection(ClusterStateProvider stateProvider, String c) {
String p = stateProvider.getPolicyNameByCollection(c);
if (p != null) {
List<Clause> perCollPolicy = policies.get(p);
if (perCollPolicy == null) {
return;
}
}
expandedClauses.addAll(mergePolicies(c, policies.getOrDefault(p, emptyList()), clusterPolicy));
}
Session copy() {
return new Session(nodes, cloudManager, getMatrixCopy(), expandedClauses, znodeVersion, nodeStateProvider);
}
public Row getNode(String node) {
for (Row row : matrix) if (row.node.equals(node)) return row;
return null;
}
List<Row> getMatrixCopy() {
return matrix.stream()
.map(row -> row.copy(this))
.collect(Collectors.toList());
}
Policy getPolicy() {
return Policy.this;
}
/**
* Apply the preferences and conditions
*/
void applyRules() {
setApproxValuesAndSortNodes(clusterPreferences, matrix);
for (Clause clause : expandedClauses) {
List<Violation> errs = clause.test(matrix);
violations.addAll(errs);
}
}
public List<Violation> getViolations() {
return violations;
}
public Suggester getSuggester(CollectionAction action) {
Suggester op = ops.get(action).get();
if (op == null) throw new UnsupportedOperationException(action.toString() + "is not supported");
op._init(this);
return op;
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
ew.put("znodeVersion", znodeVersion);
for (Row row : matrix) {
ew.put(row.node, row);
}
}
@Override
public String toString() {
return Utils.toJSONString(toMap(new LinkedHashMap<>()));
}
public List<Row> getSorted() {
return Collections.unmodifiableList(matrix);
}
public NodeStateProvider getNodeStateProvider() {
return nodeStateProvider;
}
public int indexOf(String node) {
for (int i = 0; i < matrix.size(); i++) if (matrix.get(i).node.equals(node)) return i;
throw new RuntimeException("NO such node found " + node);
}
public static Map<String, List<Clause>> policiesFromMap(Map<String, List<Map<String, Object>>> map, List<String> newParams) {
Map<String, List<Clause>> newPolicies = new HashMap<>();
map.forEach((s, l1) ->
newPolicies.put(s, l1.stream()
.map(Clause::create)
.filter(clause -> {
if (!clause.isPerCollectiontag())
throw new RuntimeException(clause.getGlobalTag().name + " is only allowed in 'cluster-policy'");
clause.addTags(newParams);
return true;
})
.sorted()
.collect(collectingAndThen(toList(), Collections::unmodifiableList))));
return newPolicies;
}
static void setApproxValuesAndSortNodes(List<Preference> clusterPreferences, List<Row> matrix) {
@ -408,8 +284,24 @@ public class Policy implements MapWriter {
}
}
/**
* Insert the collection name into the clauses where collection is not specified
*/
static List<Clause> insertColl(String coll, Collection<Clause> conditions) {
return conditions.stream()
.filter(Clause::isPerCollectiontag)
.map(clause -> {
Map<String, Object> copy = new LinkedHashMap<>(clause.original);
if (!copy.containsKey("collection")) copy.put("collection", coll);
return Clause.create(copy);
})
.filter(it -> (it.getCollection().isPass(coll)))
.collect(Collectors.toList());
}
public Session createSession(SolrCloudManager cloudManager) {
return new Session(cloudManager);
return createSession(cloudManager, null);
}
public enum SortParam {
@ -446,21 +338,8 @@ public class Policy implements MapWriter {
}
}
public static Map<String, List<Clause>> policiesFromMap(Map<String, List<Map<String, Object>>> map, List<String> newParams) {
Map<String, List<Clause>> newPolicies = new HashMap<>();
map.forEach((s, l1) ->
newPolicies.put(s, l1.stream()
.map(Clause::new)
.filter(clause -> {
if (!clause.isPerCollectiontag())
throw new RuntimeException(clause.globalTag.name + " is only allowed in 'cluster-policy'");
clause.addTags(newParams);
return true;
})
.sorted()
.collect(collectingAndThen(toList(), Collections::unmodifiableList))));
return newPolicies;
private Session createSession(SolrCloudManager cloudManager, Transaction tx) {
return new Session(cloudManager, tx);
}
public static List<Clause> mergePolicies(String coll,
@ -475,20 +354,51 @@ public class Policy implements MapWriter {
return merged;
}
/**
* Insert the collection name into the clauses where collection is not specified
*/
static List<Clause> insertColl(String coll, Collection<Clause> conditions) {
return conditions.stream()
.filter(Clause::isPerCollectiontag)
.map(clause -> {
Map<String, Object> copy = new LinkedHashMap<>(clause.original);
if (!copy.containsKey("collection")) copy.put("collection", coll);
return new Clause(copy);
})
.filter(it -> (it.collection.isPass(coll)))
.collect(Collectors.toList());
static class Transaction {
private final Policy policy;
private boolean open = false;
private Session firstSession;
private Session currentSession;
public Transaction(Policy config) {
this.policy = config;
}
public Session open(SolrCloudManager cloudManager) {
firstSession = currentSession = policy.createSession(cloudManager, Transaction.this);
open = true;
return firstSession;
}
public boolean isOpen() {
return open;
}
List<Violation> close() {
if (!open) throw new RuntimeException("Already closed");
open = false;
return currentSession.getViolations();
}
public boolean undo() {
if (currentSession.parent != null) {
currentSession = currentSession.parent;
return true;
}
return false;
}
public Session getCurrentSession() {
return currentSession;
}
void updateSession(Session session) {
currentSession = session;
}
}
private static final Map<CollectionAction, Supplier<Suggester>> ops = new HashMap<>();
@ -528,4 +438,150 @@ public class Policy implements MapWriter {
public String toString() {
return Utils.toJSONString(this);
}
/*This stores the logical state of the system, given a policy and
* a cluster state.
*
*/
public class Session implements MapWriter {
final List<String> nodes;
final SolrCloudManager cloudManager;
final List<Row> matrix;
final NodeStateProvider nodeStateProvider;
final int znodeVersion;
Set<String> collections = new HashSet<>();
List<Clause> expandedClauses;
List<Violation> violations = new ArrayList<>();
Transaction transaction;
private Session parent = null;
private Session(List<String> nodes, SolrCloudManager cloudManager,
List<Row> matrix, List<Clause> expandedClauses, int znodeVersion,
NodeStateProvider nodeStateProvider, Transaction transaction, Session parent) {
this.parent = parent;
this.transaction = transaction;
this.nodes = nodes;
this.cloudManager = cloudManager;
this.matrix = matrix;
this.expandedClauses = expandedClauses;
this.znodeVersion = znodeVersion;
this.nodeStateProvider = nodeStateProvider;
for (Row row : matrix) row.session = this;
}
Session(SolrCloudManager cloudManager, Transaction transaction) {
this.transaction = transaction;
ClusterState state = null;
this.nodeStateProvider = cloudManager.getNodeStateProvider();
try {
state = cloudManager.getClusterStateProvider().getClusterState();
LOG.trace("-- session created with cluster state: {}", state);
} catch (Exception e) {
LOG.trace("-- session created, can't obtain cluster state", e);
}
this.znodeVersion = state != null ? state.getZNodeVersion() : -1;
this.nodes = new ArrayList<>(cloudManager.getClusterStateProvider().getLiveNodes());
this.cloudManager = cloudManager;
for (String node : nodes) {
collections.addAll(nodeStateProvider.getReplicaInfo(node, Collections.emptyList()).keySet());
}
expandedClauses = clusterPolicy.stream()
.filter(clause -> !clause.isPerCollectiontag())
.collect(Collectors.toList());
ClusterStateProvider stateProvider = cloudManager.getClusterStateProvider();
for (String c : collections) {
addClausesForCollection(stateProvider, c);
}
Collections.sort(expandedClauses);
matrix = new ArrayList<>(nodes.size());
for (String node : nodes) matrix.add(new Row(node, params, perReplicaAttributes, this));
applyRules();
}
void addClausesForCollection(ClusterStateProvider stateProvider, String c) {
String p = stateProvider.getPolicyNameByCollection(c);
if (p != null) {
List<Clause> perCollPolicy = policies.get(p);
if (perCollPolicy == null) {
return;
}
}
expandedClauses.addAll(mergePolicies(c, policies.getOrDefault(p, emptyList()), clusterPolicy));
}
Session copy() {
return new Session(nodes, cloudManager, getMatrixCopy(), expandedClauses, znodeVersion, nodeStateProvider, transaction, this);
}
public Row getNode(String node) {
for (Row row : matrix) if (row.node.equals(node)) return row;
return null;
}
List<Row> getMatrixCopy() {
return matrix.stream()
.map(row -> row.copy(this))
.collect(Collectors.toList());
}
Policy getPolicy() {
return Policy.this;
}
/**
* Apply the preferences and conditions
*/
void applyRules() {
setApproxValuesAndSortNodes(clusterPreferences, matrix);
for (Clause clause : expandedClauses) {
List<Violation> errs = clause.test(this);
violations.addAll(errs);
}
}
public List<Violation> getViolations() {
return violations;
}
public Suggester getSuggester(CollectionAction action) {
Suggester op = ops.get(action).get();
if (op == null) throw new UnsupportedOperationException(action.toString() + "is not supported");
op._init(this);
return op;
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
ew.put("znodeVersion", znodeVersion);
for (Row row : matrix) {
ew.put(row.node, row);
}
}
@Override
public String toString() {
return Utils.toJSONString(toMap(new LinkedHashMap<>()));
}
public List<Row> getSorted() {
return Collections.unmodifiableList(matrix);
}
public NodeStateProvider getNodeStateProvider() {
return nodeStateProvider;
}
public int indexOf(String node) {
for (int i = 0; i < matrix.size(); i++) if (matrix.get(i).node.equals(node)) return i;
throw new RuntimeException("NO such node found " + node);
}
}
}

View File

@ -77,7 +77,7 @@ public class Preference implements MapWriter {
next.compare(r1, r2, useApprox)) : sort.sortval * result;
}
private int compareWithTolerance(Double o1, Double o2, int percentage) {
static int compareWithTolerance(Double o1, Double o2, int percentage) {
if (percentage == 0) return o1.compareTo(o2);
if (o1.equals(o2)) return 0;
double delta = Math.abs(o1 - o2);

View File

@ -0,0 +1,29 @@
/*
* 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.client.solrj.cloud.autoscaling;
import java.util.function.Function;
/**
* This clause is an instance with no conditions with computed value
*/
public class SealedClause extends Clause {
SealedClause(Clause clause, Function<Condition, Object> computedValueEvaluator) {
super(clause, computedValueEvaluator);
}
}

View File

@ -129,6 +129,9 @@ public abstract class Suggester implements MapWriter {
this.operation = init();
isInitialized = true;
}
if (operation != null && session.transaction != null && session.transaction.isOpen()) {
session.transaction.updateSession(session);
}
return operation;
}
@ -178,9 +181,15 @@ public abstract class Suggester implements MapWriter {
for (int i = 0; i < fresh.size(); i++) {
Violation freshViolation = fresh.get(i);
Violation oldViolation = null;
for (Violation v : old) {
for (Violation v : old) {//look for exactly same clause being violated
if (v.equals(freshViolation)) oldViolation = v;
}
if (oldViolation == null) {//if no match, look for similar violation
for (Violation v : old) {
if (v.isSimilarViolation(freshViolation)) oldViolation = v;
}
}
if (oldViolation != null && freshViolation.isLessSerious(oldViolation)) return true;
}
}
@ -188,7 +197,10 @@ public abstract class Suggester implements MapWriter {
}
boolean containsNewErrors(List<Violation> violations) {
boolean isTxOpen = session.transaction != null && session.transaction.isOpen();
for (Violation v : violations) {
//the computed value can change over time. So it's better to evaluate it in the end
if (isTxOpen && v.getClause().hasComputedValue) continue;
int idx = originalViolations.indexOf(v);
if (idx < 0 /*|| originalViolations.get(idx).isLessSerious(v)*/) return true;
}
@ -221,12 +233,12 @@ public abstract class Suggester implements MapWriter {
}
}
List<Violation> testChangedMatrix(boolean strict, List<Row> rows) {
Policy.setApproxValuesAndSortNodes(session.getPolicy().clusterPreferences, rows);
List<Violation> testChangedMatrix(boolean strict, Policy.Session session) {
Policy.setApproxValuesAndSortNodes(session.getPolicy().clusterPreferences, session.matrix);
List<Violation> errors = new ArrayList<>();
for (Clause clause : session.expandedClauses) {
if (strict || clause.strict) {
List<Violation> errs = clause.test(rows);
List<Violation> errs = clause.test(session);
if (!errs.isEmpty()) {
errors.addAll(errs);
}

View File

@ -26,6 +26,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -52,28 +54,24 @@ public class Suggestion {
return info;
}
static class ViolationCtx {
String tagKey;
Clause clause;
ReplicaCount count;
Violation currentViolation;
List<Row> allRows;
List<Violation> allViolations = new ArrayList<>();
public ViolationCtx(Clause clause, List<Row> allRows) {
this.allRows = allRows;
this.clause = clause;
}
public ViolationCtx reset(String tagKey, ReplicaCount count, Violation currentViolation) {
this.tagKey = tagKey;
this.count = count;
this.currentViolation = currentViolation;
allViolations.add(currentViolation);
return this;
private static Object getOperandAdjustedValue(Object val, Object original) {
if (original instanceof Clause.Condition) {
Clause.Condition condition = (Clause.Condition) original;
if (condition.computationType == null && isIntegerEquivalent(val)) {
if (condition.op == Operand.LESS_THAN) {
//replica : '<3'
val = val instanceof Long ?
(Long) val - 1 :
(Double) val - 1;
} else if (condition.op == Operand.GREATER_THAN) {
//replica : '>4'
val = val instanceof Long ?
(Long) val + 1 :
(Double) val + 1;
}
}
}
return val;
}
@ -103,6 +101,23 @@ public class Suggestion {
}
}
static boolean isIntegerEquivalent(Object val) {
if (val instanceof Number) {
Number number = (Number) val;
return Math.ceil(number.doubleValue()) == Math.floor(number.doubleValue());
} else if (val instanceof String) {
try {
double dval = Double.parseDouble((String) val);
return Math.ceil(dval) == Math.floor(dval);
} catch (NumberFormatException e) {
return false;
}
} else {
return false;
}
}
public static final Map<String, String> tagVsPerReplicaVal = Stream.of(ConditionType.values())
.filter(tag -> tag.perReplicaValue != null)
@ -115,7 +130,63 @@ public class Suggestion {
COLL("collection", String.class, null, null, null),
SHARD("shard", String.class, null, null, null),
REPLICA("replica", Long.class, null, 0L, null),
REPLICA("replica", Double.class, null, 0L, null) {
@Override
public Object validate(String name, Object val, boolean isRuleVal) {
return getOperandAdjustedValue(super.validate(name, val, isRuleVal), val);
}
@Override
public Operand getOperand(Operand expected, Object strVal, Clause.ComputationType computationType) {
if (strVal instanceof String) {
String s = ((String) strVal).trim();
int hyphenIdx = s.indexOf('-');
if (hyphenIdx > 0) {
if (hyphenIdx == s.length() - 1) {
throw new IllegalArgumentException("bad range input :" + expected);
}
if (expected == Operand.EQUAL) return Operand.RANGE_EQUAL;
if (expected == Operand.NOT_EQUAL) return Operand.RANGE_NOT_EQUAL;
}
}
if (expected == Operand.EQUAL && (computationType != null || !isIntegerEquivalent(strVal))) {
return Operand.RANGE_EQUAL;
}
if (expected == Operand.NOT_EQUAL && (computationType != null || !isIntegerEquivalent(strVal)))
return Operand.RANGE_NOT_EQUAL;
return expected;
}
@Override
public boolean supportComputed(Clause.ComputationType computedType) {
return computedType == Clause.ComputationType.PERCENT;
}
@Override
public Object computeValue(Policy.Session session, Clause.Condition cv, String collection, String shard) {
if (cv.computationType == Clause.ComputationType.PERCENT) {
AtomicInteger totalReplicasOfInterest = new AtomicInteger(0);
Clause clause = cv.getClause();
for (Row row : session.matrix) {
row.forEachReplica(replicaInfo -> {
if (replicaInfo.getCollection().equals(collection)) {
if (clause.getShard().op == Operand.WILDCARD || replicaInfo.getShard().equals(shard)) {
totalReplicasOfInterest.incrementAndGet();
}
}
});
}
return totalReplicasOfInterest.doubleValue() * Clause.parseDouble(cv.name, cv.val).doubleValue() / 100;
} else {
throw new RuntimeException("Unsupported type " + cv.computationType);
}
}
},
PORT(ImplicitSnitch.PORT, Long.class, null, 1L, 65535L),
IP_1("ip_1", Long.class, null, 0L, 255L),
IP_2("ip_2", Long.class, null, 0L, 255L),
@ -133,9 +204,10 @@ public class Suggestion {
@Override
public int compareViolation(Violation v1, Violation v2) {
return Long.compare(
v1.getViolatingReplicas().stream().mapToLong(v -> v.delta == null? 0 :v.delta).max().orElse(0L),
v2.getViolatingReplicas().stream().mapToLong(v3 -> v3.delta == null? 0 : v3.delta).max().orElse(0L));
//TODO use tolerance compare
return Double.compare(
v1.getViolatingReplicas().stream().mapToDouble(v -> v.delta == null ? 0 : v.delta).max().orElse(0d),
v2.getViolatingReplicas().stream().mapToDouble(v3 -> v3.delta == null ? 0 : v3.delta).max().orElse(0d));
}
@Override
@ -162,9 +234,9 @@ public class Suggestion {
if (s1 != null && s2 != null) return s1.compareTo(s2);
return 0;
});
long currentDelta = ctx.violation.getClause().tag.delta(node.getVal(ImplicitSnitch.DISK));
double currentDelta = ctx.violation.getClause().tag.delta(node.getVal(ImplicitSnitch.DISK));
for (ReplicaInfo replica : replicas) {
if (currentDelta <= 0) break;
if (currentDelta < 1) break;
if (replica.getVariables().get(ConditionType.CORE_IDX.tagName) == null) continue;
Suggester suggester = ctx.session.getSuggester(MOVEREPLICA)
.hint(Suggester.Hint.COLL_SHARD, new Pair<>(replica.getCollection(), replica.getShard()))
@ -214,12 +286,18 @@ public class Suggestion {
},
NODE_ROLE(ImplicitSnitch.NODEROLE, String.class, Collections.singleton("overseer"), null, null),
CORES(ImplicitSnitch.CORES, Long.class, null, 0L, Long.MAX_VALUE) {
@Override
public Object validate(String name, Object val, boolean isRuleVal) {
return getOperandAdjustedValue(super.validate(name, val, isRuleVal), val);
}
@Override
public void addViolatingReplicas(ViolationCtx ctx) {
for (Row r : ctx.allRows) {
if (!ctx.clause.tag.isPass(r)) {
r.forEachReplica(replicaInfo -> ctx.currentViolation
.addReplica(new ReplicaInfoAndErr(replicaInfo).withDelta(ctx.clause.tag.delta(r.getVal(ImplicitSnitch.CORES)))));
.addReplica(new ReplicaInfoAndErr(replicaInfo)
.withDelta(ctx.clause.tag.delta(r.getVal(ImplicitSnitch.CORES)))));
}
}
@ -330,21 +408,30 @@ public class Suggestion {
}
}
public Operand getOperand(Operand expected, Object val, Clause.ComputationType computationType) {
return expected;
}
public Object convertVal(Object val) {
return val;
}
public Object validate(String name, Object val, boolean isRuleVal) {
if (val instanceof Clause.Condition) {
Clause.Condition condition = (Clause.Condition) val;
val = condition.op.readRuleValue(condition);
if (val != condition.val) return val;
}
if (name == null) name = this.tagName;
if (type == Double.class) {
Double num = Clause.parseDouble(name, val);
if (isRuleVal) {
if (min != null)
if (Double.compare(num, (Double) min) == -1)
if (Double.compare(num, min.doubleValue()) == -1)
throw new RuntimeException(name + ": " + val + " must be greater than " + min);
if (max != null)
if (Double.compare(num, (Double) max) == 1)
if (Double.compare(num, max.doubleValue()) == 1)
throw new RuntimeException(name + ": " + val + " must be less than " + max);
}
return num;
@ -383,6 +470,39 @@ public class Suggestion {
if (Math.abs(v1.replicaCountDelta) == Math.abs(v2.replicaCountDelta)) return 0;
return Math.abs(v1.replicaCountDelta) < Math.abs(v2.replicaCountDelta) ? -1 : 1;
}
public boolean supportComputed(Clause.ComputationType computedType) {
return false;
}
public Object computeValue(Policy.Session session, Clause.Condition condition, String collection, String shard) {
return condition.val;
}
}
static class ViolationCtx {
final Function<Clause.Condition, Object> evaluator;
String tagKey;
Clause clause;
ReplicaCount count;
Violation currentViolation;
List<Row> allRows;
List<Violation> allViolations = new ArrayList<>();
public ViolationCtx(Clause clause, List<Row> allRows, Function<Clause.Condition, Object> evaluator) {
this.allRows = allRows;
this.clause = clause;
this.evaluator = evaluator;
}
public ViolationCtx reset(String tagKey, ReplicaCount count, Violation currentViolation) {
this.tagKey = tagKey;
this.count = count;
this.currentViolation = currentViolation;
allViolations.add(currentViolation);
this.clause = currentViolation.getClause();
return this;
}
}
private static void perNodeSuggestions(SuggestionCtx ctx) {

View File

@ -30,13 +30,13 @@ import org.apache.solr.common.util.Utils;
public class Violation implements MapWriter {
final String shard, coll, node;
final Object actualVal;
final Long replicaCountDelta;//how far is the actual value from the expected value
final Double replicaCountDelta;//how far is the actual value from the expected value
final Object tagKey;
private final int hash;
private final Clause clause;
private List<ReplicaInfoAndErr> replicaInfoAndErrs = new ArrayList<>();
Violation(Clause clause, String coll, String shard, String node, Object actualVal, Long replicaCountDelta, Object tagKey) {
Violation(SealedClause clause, String coll, String shard, String node, Object actualVal, Double replicaCountDelta, Object tagKey) {
this.clause = clause;
this.shard = shard;
this.coll = coll;
@ -61,19 +61,60 @@ public class Violation implements MapWriter {
}
public boolean matchShard(String shard) {
if (getClause().shard.op == Operand.WILDCARD) return true;
if (getClause().getShard().op == Operand.WILDCARD) return true;
return this.shard == null || this.shard.equals(shard);
}
//if the delta is lower , this violation is less serious
public boolean isLessSerious(Violation that) {
return this.getClause().getTag().varType.compareViolation(this, that) < 0;
}
@Override
public int hashCode() {
return hash;
}
public boolean isSimilarViolation(Violation that) {
if (Objects.equals(this.shard, that.shard) &&
Objects.equals(this.coll, that.coll) &&
Objects.equals(this.node, that.node)) {
if (this.clause.isPerCollectiontag() && that.clause.isPerCollectiontag()) {
return Objects.equals(this.clause.tag.getName(), that.clause.tag.getName());
} else if (!this.clause.isPerCollectiontag() && !that.clause.isPerCollectiontag()) {
return Objects.equals(this.clause.globalTag.getName(), that.clause.globalTag.getName())
&& Objects.equals(this.node, that.node);
} else {
return false;
}
} else {
return false;
}
}
@Override
public boolean equals(Object that) {
if (that instanceof Violation) {
Violation v = (Violation) that;
return Objects.equals(this.shard, v.shard) &&
Objects.equals(this.coll, v.coll) &&
// Objects.equals(this.node, v.node) &&
Objects.equals(this.clause, v.clause)
;
}
return false;
}
static class ReplicaInfoAndErr implements MapWriter{
final ReplicaInfo replicaInfo;
Double delta;
ReplicaInfoAndErr(ReplicaInfo replicaInfo) {
this.replicaInfo = replicaInfo;
}
Long delta;
public ReplicaInfoAndErr withDelta(Long delta) {
public ReplicaInfoAndErr withDelta(Double delta) {
this.delta = delta;
return this;
}
@ -85,28 +126,6 @@ public class Violation implements MapWriter {
}
}
@Override
public int hashCode() {
return hash;
}
//if the delta is lower , this violation is less serious
public boolean isLessSerious(Violation that) {
return this.getClause().tag.varType.compareViolation(this,that) <0 ;
}
@Override
public boolean equals(Object that) {
if (that instanceof Violation) {
Violation v = (Violation) that;
return Objects.equals(this.shard, v.shard) &&
Objects.equals(this.coll, v.coll) &&
Objects.equals(this.node, v.node) &&
Objects.equals(this.clause, v.clause)
;
}
return false;
}
@Override
public String toString() {
return Utils.toJSONString(Utils.getDeepCopy(toMap(new LinkedHashMap<>()), 5));

View File

@ -31,6 +31,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import com.google.common.collect.ImmutableList;
@ -41,6 +42,7 @@ import org.apache.solr.client.solrj.cloud.DistribStateManager;
import org.apache.solr.client.solrj.cloud.DistributedQueueFactory;
import org.apache.solr.client.solrj.cloud.NodeStateProvider;
import org.apache.solr.client.solrj.cloud.SolrCloudManager;
import org.apache.solr.client.solrj.cloud.autoscaling.Clause.RangeVal;
import org.apache.solr.client.solrj.cloud.autoscaling.Suggester.Hint;
import org.apache.solr.client.solrj.impl.ClusterStateProvider;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
@ -66,6 +68,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.client.solrj.cloud.autoscaling.Suggestion.ConditionType.FREEDISK;
import static org.apache.solr.client.solrj.cloud.autoscaling.Suggestion.ConditionType.REPLICA;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDREPLICA;
import static org.apache.solr.common.params.CollectionParams.CollectionAction.MOVEREPLICA;
@ -160,7 +163,7 @@ public class TestPolicy extends SolrTestCaseJ4 {
public void testValidate() {
expectError("replica", -1, "must be greater than");
expectError("replica", "hello", "not a valid number");
assertEquals(1L, Clause.validate("replica", "1", true));
assertEquals(1d, Clause.validate("replica", "1", true));
assertEquals("c", Clause.validate("collection", "c", true));
assertEquals("s", Clause.validate("shard", "s", true));
assertEquals("overseer", Clause.validate("nodeRole", "overseer", true));
@ -195,6 +198,63 @@ public class TestPolicy extends SolrTestCaseJ4 {
expectError("cores", "-1", "must be greater than");
assertEquals(Operand.EQUAL, REPLICA.getOperand(Operand.EQUAL, "2.0", null));
assertEquals(Operand.NOT_EQUAL, REPLICA.getOperand(Operand.NOT_EQUAL, "2.0", null));
assertEquals(Operand.EQUAL, REPLICA.getOperand(Operand.EQUAL, "2", null));
assertEquals(Operand.NOT_EQUAL, REPLICA.getOperand(Operand.NOT_EQUAL, "2", null));
assertEquals(Operand.RANGE_EQUAL, REPLICA.getOperand(Operand.EQUAL, "2.1", null));
assertEquals(Operand.RANGE_NOT_EQUAL, REPLICA.getOperand(Operand.NOT_EQUAL, "2.1", null));
assertEquals(Operand.RANGE_EQUAL, REPLICA.getOperand(Operand.EQUAL, "2.01", null));
assertEquals(Operand.RANGE_NOT_EQUAL, REPLICA.getOperand(Operand.NOT_EQUAL, "2.01", null));
Clause clause = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica: '1.23', node:'#ANY'}"));
assertTrue(clause.getReplica().isPass(2));
assertTrue(clause.getReplica().isPass(1));
assertFalse(clause.getReplica().isPass(0));
assertFalse(clause.getReplica().isPass(3));
clause = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica: '<1.23', node:'#ANY'}"));
assertTrue(clause.getReplica().isPass(1));
assertTrue(clause.getReplica().isPass(0));
assertFalse(clause.getReplica().isPass(2));
clause = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica: '!1.23', node:'#ANY'}"));
assertFalse(clause.getReplica().isPass(2));
assertFalse(clause.getReplica().isPass(1));
assertTrue(clause.getReplica().isPass(0));
assertTrue(clause.getReplica().isPass(3));
clause = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica: 1.23, node:'#ANY'}"));
assertTrue(clause.getReplica().isPass(2));
assertTrue(clause.getReplica().isPass(1));
assertFalse(clause.getReplica().isPass(0));
assertFalse(clause.getReplica().isPass(3));
clause = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica: '33%', node:'#ANY'}"));
assertEquals(Operand.RANGE_EQUAL, clause.getReplica().op);
clause = clause.getSealedClause(condition -> {
if (condition.name.equals("replica")) {
return 2.0d;
}
throw new RuntimeException("");
});
assertTrue( clause.getReplica().isPass(2));
clause = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica: '3 - 5', node:'#ANY'}"));
assertEquals(Operand.RANGE_EQUAL, clause.getReplica().getOperand());
RangeVal range = (RangeVal) clause.getReplica().getValue();
assertEquals(3.0 , range.min);
assertEquals(5.0 , range.max);
assertTrue(clause.replica.isPass(3));
assertTrue(clause.replica.isPass(4));
assertTrue(clause.replica.isPass(5));
assertFalse(clause.replica.isPass(6));
assertFalse(clause.replica.isPass(2));
assertEquals(new Double(1.0), clause.replica.delta(6));
assertEquals(new Double(-1.0), clause.replica.delta(2));
assertEquals(new Double(0.0), clause.replica.delta(4));
}
@ -209,39 +269,50 @@ public class TestPolicy extends SolrTestCaseJ4 {
}
public void testOperands() {
Clause c = new Clause((Map<String, Object>) Utils.fromJSONString("{replica:'<2', node:'#ANY'}"));
Clause c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:'<2', node:'#ANY'}"));
assertFalse(c.replica.isPass(3));
assertFalse(c.replica.isPass(2));
assertTrue(c.replica.isPass(1));
assertEquals("{\"replica\":\"<2.0\"}", c.replica.toString());
c = new Clause((Map<String, Object>) Utils.fromJSONString("{replica:'>2', node:'#ANY'}"));
c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:'>2', node:'#ANY'}"));
assertTrue(c.replica.isPass(3));
assertFalse(c.replica.isPass(2));
assertFalse(c.replica.isPass(1));
assertEquals("{\"replica\":\">2.0\"}", c.replica.toString());
c = new Clause((Map<String, Object>) Utils.fromJSONString("{replica:0, nodeRole:'!overseer'}"));
c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:0, nodeRole:'!overseer'}"));
assertTrue(c.tag.isPass("OVERSEER"));
assertFalse(c.tag.isPass("overseer"));
c = new Clause((Map<String, Object>) Utils.fromJSONString("{replica:0, sysLoadAvg:'<12.7'}"));
c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:0, sysLoadAvg:'<12.7'}"));
assertTrue(c.tag.isPass("12.6"));
assertTrue(c.tag.isPass(12.6d));
assertFalse(c.tag.isPass("12.9"));
assertFalse(c.tag.isPass(12.9d));
c = new Clause((Map<String, Object>) Utils.fromJSONString("{replica:0, sysLoadAvg:'>12.7'}"));
c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:0, sysLoadAvg:'>12.7'}"));
assertTrue(c.tag.isPass("12.8"));
assertTrue(c.tag.isPass(12.8d));
assertFalse(c.tag.isPass("12.6"));
assertFalse(c.tag.isPass(12.6d));
c = new Clause((Map<String, Object>) Utils.fromJSONString("{replica:0, 'metrics:x:y:z':'>12.7'}"));
c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:0, 'metrics:x:y:z':'>12.7'}"));
assertTrue(c.tag.val instanceof String);
assertTrue(c.tag.isPass("12.8"));
assertTrue(c.tag.isPass(12.8d));
assertFalse(c.tag.isPass("12.6"));
assertFalse(c.tag.isPass(12.6d));
try {
c = Clause.create((Map<String, Object>) Utils.fromJSONString("{replica:0, 'ip_1':'<30%'}"));
fail("Expected exception");
} catch (Exception e) {
assertTrue(e.getMessage().contains("'%' is not allowed for variable : 'ip_1'"));
}
}
public void testNodeLost() {
@ -261,32 +332,6 @@ public class TestPolicy extends SolrTestCaseJ4 {
" 'node':'127.0.0.1:65434_solr'," +
" 'cores':0," +
" 'freedisk':884.7097854614258}}}";
/* String stateJson = "{'testNodeLost':{" +
" 'pullReplicas':'0'," +
" 'replicationFactor':'2'," +
" 'router':{'name':'compositeId'}," +
" 'maxShardsPerNode':'1'," +
" 'autoAddReplicas':'false'," +
" 'nrtReplicas':'2'," +
" 'tlogReplicas':'0'," +
" 'shards':{'shard1':{" +
" 'range':'80000000-7fffffff'," +
" 'state':'active'," +
" 'replicas':{" +
" 'core_node1':{" +
" 'core':'testNodeLost_shard1_replica_n1'," +
" 'base_url':'http://127.0.0.1:65417/solr'," +
" 'node_name':'127.0.0.1:65417_solr'," +
" 'state':'active'," +
" 'type':'NRT'," +
" 'leader':'true'}," +
" 'core_node2':{" +
" 'core':'testNodeLost_shard1_replica_n2'," +
" 'base_url':'http://127.0.0.1:65427/solr'," +
" 'node_name':'127.0.0.1:65427_solr'," +
" 'state':'down'," +
" 'type':'NRT'}}}}}}";*/
String autoScalingjson = "{" +
" 'cluster-policy':[" +
" {" +
@ -746,7 +791,7 @@ public class TestPolicy extends SolrTestCaseJ4 {
assertTrue("Collection for replica is not as expected " + collection, collection.equals("newColl") || collection.equals("newColl2"));
if (collection.equals("newColl")) countNewCollOp++;
else countNewColl2Op++;
assertEquals("PULL type node must be in 'slowdisk' node", "node1", op.getParams().get("node"));
assertEquals("PULL type node must be in 'slowdisk' node, countOp : " + countOp, "node1", op.getParams().get("node"));
suggester = suggester.getSession().getSuggester(ADDREPLICA)
.hint(Hint.REPLICATYPE, Replica.Type.PULL)
.hint(Hint.COLL_SHARD, new Pair<>("newColl", "shard1"))
@ -1510,7 +1555,7 @@ public class TestPolicy extends SolrTestCaseJ4 {
assertFalse(l.isEmpty());
Map m = l.get(0).toMap(new LinkedHashMap<>());
assertEquals(1L, Utils.getObjectByPath(m, true, "violation/violation/delta"));
assertEquals(1.0d, Utils.getObjectByPath(m, true, "violation/violation/delta"));
assertEquals("POST", Utils.getObjectByPath(m, true, "operation/method"));
assertEquals("/c/mycoll1", Utils.getObjectByPath(m, true, "operation/path"));
assertNotNull(Utils.getObjectByPath(m, false, "operation/command/move-replica"));
@ -1518,6 +1563,105 @@ public class TestPolicy extends SolrTestCaseJ4 {
assertEquals("core_node2", Utils.getObjectByPath(m, true, "operation/command/move-replica/replica"));
}
public void testReplicaPercentage() {
String dataproviderdata = "{" +
" 'liveNodes':[" +
" '10.0.0.6:7574_solr'," +
" '10.0.0.6:8983_solr']," +
" 'replicaInfo':{" +
" '10.0.0.6:7574_solr':{}," +
" '10.0.0.6:8983_solr':{'mycoll1':{" +
" 'shard2':[{'core_node2':{'type':'NRT'}}]," +
" 'shard1':[{'core_node1':{'type':'NRT'}}]}}}," +
" 'nodeValues':{" +
" '10.0.0.6:7574_solr':{" +
" 'node':'10.0.0.6:7574_solr'," +
" 'cores':0}," +
" '10.0.0.6:8983_solr':{" +
" 'node':'10.0.0.6:8983_solr'," +
" 'cores':2}}}";
String autoScalingjson = " { cluster-policy:[" +
" { replica :'<51%', node:'#ANY'}]," +
" cluster-preferences :[{ minimize : cores }]}";
AutoScalingConfig autoScalingConfig = new AutoScalingConfig((Map<String, Object>) Utils.fromJSONString(autoScalingjson));
Policy.Session session = autoScalingConfig.getPolicy().createSession(cloudManagerWithData(dataproviderdata));
List<Violation> violations = session.getViolations();
assertEquals(1, violations.size());
autoScalingjson = " { cluster-policy:[" +
" { replica :'<51%', shard: '#EACH' , node:'#ANY'}]," +
" cluster-preferences :[{ minimize : cores }]}";
autoScalingConfig = new AutoScalingConfig((Map<String, Object>) Utils.fromJSONString(autoScalingjson));
session = autoScalingConfig.getPolicy().createSession(cloudManagerWithData(dataproviderdata));
violations = session.getViolations();
assertEquals(0, violations.size());
}
public void testReplicaZonesPercentage() {
String dataproviderdata = "{" +
" 'liveNodes':[" +
" '10.0.0.6:7574_solr'," +
" '10.0.0.6:8983_solr']," +
" 'replicaInfo':{" +
" '10.0.0.6:7574_solr':{}," +
" '10.0.0.6:8983_solr':{}}," +
" 'nodeValues':{" +
" '10.0.0.6:7574_solr':{" +
" 'node':'10.0.0.6:7574_solr'," +
" 'cores':0," +
" 'sysprop.az': 'west'" +
" }," +
" '10.0.0.6:8983_solr':{" +
" 'node':'10.0.0.6:8983_solr'," +
" 'cores':0," +
" 'sysprop.az': 'east' " +
" }}}";
String autoScalingjson = " { cluster-policy:[" +
" { replica :'<34%', shard: '#EACH', sysprop.az : east}," +
" { replica :'<67%', shard: '#EACH', sysprop.az : west}" +
" ]," +
" cluster-preferences :[{ minimize : cores }]}";
String COLL_NAME = "percentColl";
AutoScalingConfig autoScalingConfig = new AutoScalingConfig((Map<String, Object>) Utils.fromJSONString(autoScalingjson));
Policy.Transaction txn = new Policy.Transaction(autoScalingConfig.getPolicy());
txn.open(cloudManagerWithData(dataproviderdata));
List<String> nodes = new ArrayList<>();
for (int i = 0; i < 12; i++) {
SolrRequest suggestion = txn.getCurrentSession()
.getSuggester(ADDREPLICA)
.hint(Hint.COLL_SHARD, new Pair<>(COLL_NAME, "shard1"))
.getSuggestion();
assertNotNull(suggestion);
String node = suggestion.getParams().get("node");
nodes.add(node);
if (i % 3 == 1) assertEquals("10.0.0.6:8983_solr", node);
else assertEquals("10.0.0.6:7574_solr", node);
}
List<Violation> violations = txn.close();
assertTrue(violations.isEmpty());
Policy.Session latestSession = txn.getCurrentSession();
assertEquals("10.0.0.6:7574_solr", latestSession.matrix.get(0).node);
AtomicInteger count = new AtomicInteger();
latestSession.matrix.get(0).forEachReplica(replicaInfo -> count.incrementAndGet());
assertEquals(8, count.get());
assertEquals("10.0.0.6:8983_solr", latestSession.matrix.get(1).node);
count.set(0);
latestSession.matrix.get(1).forEachReplica(replicaInfo -> count.incrementAndGet());
assertEquals(4, count.get());
}
public void testFreeDiskSuggestions() {
String dataproviderdata = "{" +
" liveNodes:[node1,node2]," +