diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 1d7bb9ccd6f..b1adaa51a9c 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -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 ---------------------- diff --git a/solr/core/src/java/org/apache/solr/cloud/autoscaling/AutoScalingHandler.java b/solr/core/src/java/org/apache/solr/cloud/autoscaling/AutoScalingHandler.java index a69451e9c3a..17734dc16a5 100644 --- a/solr/core/src/java/org/apache/solr/cloud/autoscaling/AutoScalingHandler.java +++ b/solr/core/src/java/org/apache/solr/cloud/autoscaling/AutoScalingHandler.java @@ -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 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; diff --git a/solr/core/src/test/org/apache/solr/cloud/autoscaling/AutoScalingHandlerTest.java b/solr/core/src/test/org/apache/solr/cloud/autoscaling/AutoScalingHandlerTest.java index cf57c48f18f..afce5a49081 100644 --- a/solr/core/src/test/org/apache/solr/cloud/autoscaling/AutoScalingHandlerTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/autoscaling/AutoScalingHandlerTest.java @@ -831,7 +831,7 @@ public class AutoScalingHandlerTest extends SolrCloudTestCase { for (Map 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")); diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AddReplicaSuggester.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AddReplicaSuggester.java index 918313654e8..4226d81ba16 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AddReplicaSuggester.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/AddReplicaSuggester.java @@ -44,14 +44,15 @@ class AddReplicaSuggester extends Suggester { } for (Pair 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 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 errs = testChangedMatrix(strict, tmpRow.session.matrix); + List errs = testChangedMatrix(strict, tmpRow.session); + if (!containsNewErrors(errs)) { if (isLessSerious(errs, leastSeriousViolation)) { leastSeriousViolation = errs; diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Clause.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Clause.java index 8f198bd8bbc..d40f0d8beea 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Clause.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Clause.java @@ -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 { private static final Set IGNORE_TAGS = new HashSet<>(Arrays.asList(REPLICA, COLLECTION, SHARD, "strict", "type")); + final boolean hasComputedValue; final Map original; Condition collection, shard, replica, tag, globalTag; final Replica.Type type; - boolean strict; - public Clause(Map m) { + protected Clause(Clause clause, Function 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 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 { 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 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 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 { 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 { 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 { } } - @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 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 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 test(Policy.Session session) { + ComputedValueEvaluator computedValueEvaluator = new ComputedValueEvaluator(session); + Suggestion.ViolationCtx ctx = new Suggestion.ViolationCtx(this, session.matrix, computedValueEvaluator); + if (isPerCollectiontag()) { + Map>> replicaCount = computeReplicaCounts(session.matrix, computedValueEvaluator); + for (Map.Entry>> e : replicaCount.entrySet()) { + computedValueEvaluator.collName = e.getKey(); + if (!collection.isPass(computedValueEvaluator.collName)) continue; + for (Map.Entry> shardVsCount : e.getValue().entrySet()) { + computedValueEvaluator.shardName = shardVsCount.getKey(); + if (!shard.isPass(computedValueEvaluator.shardName)) continue; + for (Map.Entry 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>> computeReplicaCounts(List allRows, + ComputedValueEvaluator computedValueEvaluator) { + Map>> collVsShardVsTagVsCount = new HashMap<>(); + for (Row row : allRows) { + for (Map.Entry>> colls : row.collectionVsShardVsReplicas.entrySet()) { + String collectionName = colls.getKey(); + if (!collection.isPass(collectionName)) continue; + Map> collMap = collVsShardVsTagVsCount.computeIfAbsent(collectionName, s -> new HashMap<>()); + for (Map.Entry> shards : colls.getValue().entrySet()) { + String shardName = shards.getKey(); + if (ANY.equals(shard.val)) shardName = ANY; + if (!shard.isPass(shardName)) break; + Map 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 { 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 { 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 { } } - 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 test(List allRows) { - Suggestion.ViolationCtx ctx = new Suggestion.ViolationCtx(this, allRows); - if (isPerCollectiontag()) { - Map>> replicaCount = computeReplicaCounts(allRows); - for (Map.Entry>> e : replicaCount.entrySet()) { - if (!collection.isPass(e.getKey())) continue; - for (Map.Entry> shardVsCount : e.getValue().entrySet()) { - if (!shard.isPass(shardVsCount.getKey())) continue; - for (Map.Entry 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>> computeReplicaCounts(List allRows) { - Map>> collVsShardVsTagVsCount = new HashMap<>(); - for (Row row : allRows) { - for (Map.Entry>> colls : row.collectionVsShardVsReplicas.entrySet()) { - String collectionName = colls.getKey(); - if (!collection.isPass(collectionName)) continue; - Map> collMap = collVsShardVsTagVsCount.computeIfAbsent(collectionName, s -> new HashMap<>()); - for (Map.Entry> shards : colls.getValue().entrySet()) { - String shardName = shards.getKey(); - if (ANY.equals(shard.val)) shardName = ANY; - if (!shard.isPass(shardName)) break; - Map 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 { NOT_APPLICABLE, FAIL, PASS } - public static String parseString(Object val) { - return val == null ? null : String.valueOf(val); + public static class ComputedValueEvaluator implements Function { + 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 { 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); + } + } + } diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/MoveReplicaSuggester.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/MoveReplicaSuggester.java index c0521cd5d98..2843f8ac135 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/MoveReplicaSuggester.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/MoveReplicaSuggester.java @@ -60,7 +60,7 @@ public class MoveReplicaSuggester extends Suggester { Pair 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 errs = testChangedMatrix(strict, srcRowModified.session.matrix); + List errs = testChangedMatrix(strict, srcRowModified.session); srcRowModified.session.applyRules();// now resort the nodes with the new values Policy.Session tmpSession = srcRowModified.session; if (!containsNewErrors(errs) && diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Operand.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Operand.java index 11df06fccd8..bda1e1fc7bb 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Operand.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Operand.java @@ -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; + } } diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Policy.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Policy.java index 60ff0c929be..f7b8c034d08 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Policy.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Policy.java @@ -112,7 +112,7 @@ public class Policy implements MapWriter { clusterPreferences.forEach(preference -> paramsOfInterest.add(preference.name.toString())); List newParams = new ArrayList<>(paramsOfInterest); clusterPolicy = ((List>) 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 nodes; - final SolrCloudManager cloudManager; - final List matrix; - Set collections = new HashSet<>(); - List expandedClauses; - List violations = new ArrayList<>(); - final NodeStateProvider nodeStateProvider; - final int znodeVersion; - - private Session(List nodes, SolrCloudManager cloudManager, - List matrix, List 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 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 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 errs = clause.test(matrix); - violations.addAll(errs); - } - } - - - public List 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 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> policiesFromMap(Map>> map, List newParams) { + Map> 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 clusterPreferences, List matrix) { @@ -408,8 +284,24 @@ public class Policy implements MapWriter { } } + /** + * Insert the collection name into the clauses where collection is not specified + */ + static List insertColl(String coll, Collection conditions) { + return conditions.stream() + .filter(Clause::isPerCollectiontag) + .map(clause -> { + Map 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> policiesFromMap(Map>> map, List newParams) { - Map> 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 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 insertColl(String coll, Collection conditions) { - return conditions.stream() - .filter(Clause::isPerCollectiontag) - .map(clause -> { - Map 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 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> 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 nodes; + final SolrCloudManager cloudManager; + final List matrix; + final NodeStateProvider nodeStateProvider; + final int znodeVersion; + Set collections = new HashSet<>(); + List expandedClauses; + List violations = new ArrayList<>(); + Transaction transaction; + private Session parent = null; + + private Session(List nodes, SolrCloudManager cloudManager, + List matrix, List 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 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 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 errs = clause.test(this); + violations.addAll(errs); + } + } + + + public List 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 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); + } + } } diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Preference.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Preference.java index 0db9886ae7c..422f9871886 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Preference.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Preference.java @@ -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); diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SealedClause.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SealedClause.java new file mode 100644 index 00000000000..5a40b423707 --- /dev/null +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/SealedClause.java @@ -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 computedValueEvaluator) { + super(clause, computedValueEvaluator); + } +} diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggester.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggester.java index eb0c63c8124..c201aa30823 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggester.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggester.java @@ -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 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 testChangedMatrix(boolean strict, List rows) { - Policy.setApproxValuesAndSortNodes(session.getPolicy().clusterPreferences, rows); + List testChangedMatrix(boolean strict, Policy.Session session) { + Policy.setApproxValuesAndSortNodes(session.getPolicy().clusterPreferences, session.matrix); List errors = new ArrayList<>(); for (Clause clause : session.expandedClauses) { if (strict || clause.strict) { - List errs = clause.test(rows); + List errs = clause.test(session); if (!errs.isEmpty()) { errors.addAll(errs); } diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggestion.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggestion.java index c2f9d247e64..502531ba37e 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggestion.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Suggestion.java @@ -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 allRows; - - List allViolations = new ArrayList<>(); - - public ViolationCtx(Clause clause, List 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 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 evaluator; + String tagKey; + Clause clause; + ReplicaCount count; + Violation currentViolation; + List allRows; + List allViolations = new ArrayList<>(); + + public ViolationCtx(Clause clause, List allRows, Function 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) { diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Violation.java b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Violation.java index 76bd7d51e56..87b0abe7fed 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Violation.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/cloud/autoscaling/Violation.java @@ -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 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)); diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/cloud/autoscaling/TestPolicy.java b/solr/solrj/src/test/org/apache/solr/client/solrj/cloud/autoscaling/TestPolicy.java index 6d14910d8ee..a7ba67c6edd 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/cloud/autoscaling/TestPolicy.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/cloud/autoscaling/TestPolicy.java @@ -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) 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) 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) 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) 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) 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) 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) Utils.fromJSONString("{replica:'<2', node:'#ANY'}")); + Clause c = Clause.create((Map) 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) Utils.fromJSONString("{replica:'>2', node:'#ANY'}")); + c = Clause.create((Map) 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) Utils.fromJSONString("{replica:0, nodeRole:'!overseer'}")); + + c = Clause.create((Map) Utils.fromJSONString("{replica:0, nodeRole:'!overseer'}")); assertTrue(c.tag.isPass("OVERSEER")); assertFalse(c.tag.isPass("overseer")); - c = new Clause((Map) Utils.fromJSONString("{replica:0, sysLoadAvg:'<12.7'}")); + c = Clause.create((Map) 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) Utils.fromJSONString("{replica:0, sysLoadAvg:'>12.7'}")); + c = Clause.create((Map) 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) Utils.fromJSONString("{replica:0, 'metrics:x:y:z':'>12.7'}")); + c = Clause.create((Map) 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) 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) Utils.fromJSONString(autoScalingjson)); + Policy.Session session = autoScalingConfig.getPolicy().createSession(cloudManagerWithData(dataproviderdata)); + List violations = session.getViolations(); + assertEquals(1, violations.size()); + autoScalingjson = " { cluster-policy:[" + + " { replica :'<51%', shard: '#EACH' , node:'#ANY'}]," + + " cluster-preferences :[{ minimize : cores }]}"; + autoScalingConfig = new AutoScalingConfig((Map) 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) Utils.fromJSONString(autoScalingjson)); + + Policy.Transaction txn = new Policy.Transaction(autoScalingConfig.getPolicy()); + txn.open(cloudManagerWithData(dataproviderdata)); + + List 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 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]," +