joseluisll commented on code in PR #8799: URL: https://github.com/apache/hadoop/pull/8799#discussion_r4236878304
########## hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestCapacitySchedulerConfigBenchmark.java: ########## @@ -0,0 +1,234 @@ +/** + * 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.hadoop.yarn.server.resourcemanager.scheduler.capacity; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.test.GenericTestUtils; +import org.apache.hadoop.yarn.conf.YarnConfiguration; +import org.apache.hadoop.yarn.server.resourcemanager.MockRM; +import org.apache.hadoop.yarn.server.resourcemanager.NodeAttributeTestUtils; +import org.apache.hadoop.yarn.server.resourcemanager.RMContext; +import org.apache.hadoop.yarn.server.resourcemanager.scheduler.QueueMetrics; +import org.apache.hadoop.yarn.server.resourcemanager.scheduler.ResourceScheduler; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Capacity Scheduler configuration benchmark: scheduler load, queue refresh + * and configuration validation on generated trees of + * {@link CSConfigBenchmarkGenerator}. + * + * <p>It only runs with {@code -DRunCapacitySchedulerConfigBenchmark=true}. + * Each size should run in its own JVM, and each fork is a separate Maven + * invocation, for example three forks of every size:</p> + * <pre> + * RM=hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager + * for fork in 1 2 3; do for size in 10 100 1000 5000; do + * ./mvnw -o test -pl $RM -Dtest=TestCapacitySchedulerConfigBenchmark \ + * -DRunCapacitySchedulerConfigBenchmark=true -Dcs.bench.sizes=$size \ + * -Dcs.bench.fork=$fork -Dcs.bench.out=/tmp/cs-bench.txt + * done; done + * </pre> + * <p>Properties:</p> + * <ul> + * <li>{@code cs.bench.sizes}: requested queue counts, default + * {@code 10,100,1000,5000};</li> + * <li>{@code cs.bench.warmups} (default 2) and {@code cs.bench.iterations} + * (default 5) per operation; the median of the iterations is reported;</li> + * <li>{@code cs.bench.ops}: subset of {@code scheduler-load}, + * {@code refresh} and {@code validate}, default all;</li> + * <li>{@code cs.bench.validators}: implementations measured by the + * {@code validate} operation, default {@code legacy} + * ({@link CapacitySchedulerConfigValidator#validateCSConfiguration}, the + * core of {@code POST /scheduler-conf/validate}); any other entry is the + * class name of a {@link ProposalValidator} with a no-argument constructor, + * which is the hook for {@code validate/v2};</li> + * <li>{@code cs.bench.capacityMode}: a + * {@link CSConfigBenchmarkGenerator.CapacityMode}, default + * {@code PCT_WEIGHT_MIXED};</li> + * <li>{@code cs.bench.fork}: label printed with the results;</li> + * <li>{@code cs.bench.out}: file the {@code BENCH} result lines are + * appended to (they are always printed to standard output).</li> + * </ul> + * <p>Operations: {@code scheduler-load} initializes a fresh + * {@link CapacityScheduler} from the configuration, {@code refresh} + * reinitializes the running scheduler with the unchanged configuration (what + * {@code AdminService.refreshQueues} does after loading the file), and + * {@code validate} validates a proposal that moves one percent of capacity Review Comment: Nit: in `ALL_ABSOLUTE` mode `createMutatedCopy` falls back to changing `maximum-applications` rather than moving capacity, so the validate proposal is not a one percent capacity move there. Worth mentioning in the javadoc. ########## hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/CSConfigBenchmarkGenerator.java: ########## @@ -0,0 +1,543 @@ +/** + * 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.hadoop.yarn.server.resourcemanager.scheduler.capacity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.yarn.api.records.QueueACL; + +import static org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableSet.of; + +/** + * Generates synthetic but structurally realistic capacity-scheduler + * configuration content for a requested queue count. + * It feeds {@link TestCapacitySchedulerConfigBenchmark} (scheduler load, + * refresh and validation timings) and {@link TestCSConfigBenchmarkGenerator}. + * + * Tree shape (mixed depth 3 and 4): + * <pre> + * root + * p0 (pct children, node label, one legacy managed parent leaf) + * p1 (weight children, AQC v2 template on sub-parent s0) + * p2 (pct children, unlabeled) + * p3 (weight children) + * ... + * each pI: 2 direct leaves + up to 5 sub-parents with up to 9 leaves each + * </pre> + * + * Capacity modes: + * - PCT_WEIGHT_MIXED (default): root children are percentage; even-indexed + * subtrees use percentage children, odd-indexed use weight children; the last + * sub-parent of a weight subtree flips its children back to percentage + * (legal per the legacy-mode parent/child mode table in AbstractParentQueue). + * - ALL_ABSOLUTE: every queue uses absolute [memory=..,vcores=..] resources, + * with parent minimums equal to the sum of their children. This is a separate + * mode because in legacy queue mode absolute resources cannot be mixed with + * percentage/weight anywhere in one hierarchy (absolute children require an + * absolute parent, and siblings must be mode-uniform). + * + * Extras sprinkled for realism: accessible-node-labels + per-label capacities + * on every fourth subtree (labels "blue"/"ssd"), legacy managed-parent + * (auto-create-child-queue) leaf templates, auto-queue-creation-v2 templates, + * legacy queue-mapping placement rules, ACLs, user-limit-factor, + * maximum-capacity and ordering policies on a subset of queues. + */ +public final class CSConfigBenchmarkGenerator { + + public enum CapacityMode { + PCT_WEIGHT_MIXED, + ALL_ABSOLUTE + } + + /** Everything the harness needs to know about a generated config. */ + public static final class GeneratedConfig { + private final CapacitySchedulerConfiguration conf; + private final int queueCount; + private final List<String> leafPaths; + private final String mutationLeafA; + private final String mutationLeafB; + private final List<String> labels; + + private GeneratedConfig(CapacitySchedulerConfiguration conf, int queueCount, + List<String> leafPaths, String mutationLeafA, String mutationLeafB, + List<String> labels) { + this.conf = conf; + this.queueCount = queueCount; + this.leafPaths = leafPaths; + this.mutationLeafA = mutationLeafA; + this.mutationLeafB = mutationLeafB; + this.labels = labels; + } + + public CapacitySchedulerConfiguration getConf() { + return conf; + } + + /** Number of generated queues, root excluded. */ + public int getQueueCount() { + return queueCount; + } + + public List<String> getLeafPaths() { + return Collections.unmodifiableList(leafPaths); + } + + /** Two sibling leaves whose capacities can be swapped for a valid change. */ + public String getMutationLeafA() { + return mutationLeafA; + } + + public String getMutationLeafB() { + return mutationLeafB; + } + + public List<String> getLabels() { + return Collections.unmodifiableList(labels); + } + } + + private static final int DIRECT_LEAVES = 2; + private static final int SUB_PARENTS = 5; + private static final int LEAVES_PER_SUB_PARENT = 9; + private static final int MAX_MANAGED_PARENTS = 4; + private static final int MAX_AQC_V2_PARENTS = 4; + private static final String LABEL_BLUE = "blue"; + private static final String LABEL_SSD = "ssd"; + private static final String CS_PREFIX = CapacitySchedulerConfiguration.PREFIX; + + private CSConfigBenchmarkGenerator() { + } + + private static final class Node { + private final String name; + private final String path; + private final List<Node> children = new ArrayList<>(); + private boolean managedParent; + + Node(String parentPath, String name) { + this.name = name; + this.path = parentPath + "." + name; + } + + boolean isLeaf() { + return children.isEmpty() && !managedParent; + } + } + + public static GeneratedConfig generate(int targetQueueCount) { + return generate(targetQueueCount, CapacityMode.PCT_WEIGHT_MIXED); + } + + public static GeneratedConfig generate(int targetQueueCount, CapacityMode mode) { + List<Node> tops = buildForest(targetQueueCount); + CapacitySchedulerConfiguration conf = + new CapacitySchedulerConfiguration(new Configuration(false), false); + + markManagedParents(tops); + QueuePath rootPath = new QueuePath(CapacitySchedulerConfiguration.ROOT); + conf.setQueues(rootPath, names(tops)); + + if (mode == CapacityMode.ALL_ABSOLUTE) { + emitAbsolute(conf, rootPath, tops); + } else { + emitMixed(conf, tops); + } + + List<String> leafPaths = new ArrayList<>(); + int count = 0; + for (Node top : tops) { + count += collect(top, leafPaths); + } + String[] mutationPair = pickMutationSiblings(tops); + setPlacementRules(conf, leafPaths); + + List<String> labels = new ArrayList<>(); + if (mode == CapacityMode.PCT_WEIGHT_MIXED) { + for (int i = 0; i < tops.size(); i++) { + String label = labelFor(i); + if (label != null && !labels.contains(label)) { + labels.add(label); + } + } + } + return new GeneratedConfig(conf, count, leafPaths, mutationPair[0], + mutationPair[1], labels); + } + + /** + * Returns a copy of {@code base} with a valid capacity change applied: one + * percent is moved from mutation leaf B to mutation leaf A (and likewise for + * their per-label capacities), so per-parent sums stay at 100. + */ + public static Configuration createMutatedCopy(Configuration base, + GeneratedConfig gen) { + Configuration copy = new Configuration(base); + moveOnePercent(copy, gen.getMutationLeafA(), gen.getMutationLeafB(), null); + for (String label : gen.getLabels()) { + moveOnePercent(copy, gen.getMutationLeafA(), gen.getMutationLeafB(), label); + } + return copy; + } + + private static void moveOnePercent(Configuration conf, String leafA, + String leafB, String label) { + if (leafA == null || leafB == null) { + return; + } + String suffix = (label == null) ? ".capacity" + : ".accessible-node-labels." + label + ".capacity"; + String keyA = CS_PREFIX + leafA + suffix; + String keyB = CS_PREFIX + leafB + suffix; + String a = conf.get(keyA); + String b = conf.get(keyB); + if (a == null || b == null) { + return; + } + try { + conf.set(keyA, String.valueOf(Float.parseFloat(a) + 1.0f)); + conf.set(keyB, String.valueOf(Float.parseFloat(b) - 1.0f)); + } catch (NumberFormatException e) { + // Absolute/weight profiles mutate a real queue limit, never a no-op. + conf.set(CS_PREFIX + leafA + ".maximum-applications", "2345"); + } + } + + // --------------------------------------------------------------------- + // Tree construction + // --------------------------------------------------------------------- + + private static List<Node> buildForest(int targetQueueCount) { + List<Node> tops = new ArrayList<>(); + int fullSubtree = 1 + DIRECT_LEAVES + SUB_PARENTS * (1 + LEAVES_PER_SUB_PARENT); + int remaining = targetQueueCount; + int i = 0; + while (remaining > 0) { + int budget = Math.min(fullSubtree, remaining); + Node top = new Node(CapacitySchedulerConfiguration.ROOT, "p" + i); + budget--; + int directLeaves = Math.min(DIRECT_LEAVES, budget); + for (int l = 0; l < directLeaves; l++) { + top.children.add(new Node(top.path, "l" + l)); + } + budget -= directLeaves; + int s = 0; + while (budget > 0) { + Node sub = new Node(top.path, "s" + s); + top.children.add(sub); + budget--; + int subLeaves = Math.min(LEAVES_PER_SUB_PARENT, budget); + for (int l = 0; l < subLeaves; l++) { + sub.children.add(new Node(sub.path, "l" + l)); + } + budget -= subLeaves; + s++; + } + remaining -= subtreeSize(top); + tops.add(top); + i++; + } + return tops; + } + + private static int subtreeSize(Node node) { + int size = 1; + for (Node child : node.children) { + size += subtreeSize(child); + } + return size; + } + + /** + * Converts the last direct leaf of every other percentage subtree into a + * legacy managed parent (auto-create-child-queue.enabled), capped globally. + */ + private static void markManagedParents(List<Node> tops) { + int marked = 0; + for (int i = 0; i < tops.size() && marked < MAX_MANAGED_PARENTS; i += 4) { + Node top = tops.get(i); + for (int c = top.children.size() - 1; c >= 0; c--) { + Node child = top.children.get(c); + if (child.isLeaf()) { + child.managedParent = true; + marked++; + break; + } + } + } + } + + // --------------------------------------------------------------------- + // Mixed percentage/weight emission + // --------------------------------------------------------------------- + + private static void emitMixed(CapacitySchedulerConfiguration conf, List<Node> tops) { + float[] topCaps = splitPercentages(tops.size()); + Map<String, List<Integer>> labelMembers = new LinkedHashMap<>(); + for (int i = 0; i < tops.size(); i++) { + String label = labelFor(i); + if (label != null) { + labelMembers.computeIfAbsent(label, k -> new ArrayList<>()).add(i); + } + } + // Per-label percentage shares among the labeled root children; the sums per + // label are 100 among label-accessible siblings, as legacy mode requires. + Map<Integer, Float> topLabelCap = new LinkedHashMap<>(); + for (List<Integer> members : labelMembers.values()) { + float[] shares = splitPercentages(members.size()); + for (int m = 0; m < members.size(); m++) { + topLabelCap.put(members.get(m), shares[m]); + } + } + + int aqcV2Marked = 0; + for (int i = 0; i < tops.size(); i++) { + Node top = tops.get(i); + QueuePath topPath = new QueuePath(top.path); + conf.setCapacity(topPath, topCaps[i]); + conf.setMaximumCapacity(topPath, 100f); + String label = labelFor(i); + if (label != null) { + conf.setAccessibleNodeLabels(topPath, of(label)); + conf.setCapacityByLabel(topPath, label, topLabelCap.get(i)); + } + boolean weightMode = (i % 2 == 1); + boolean markAqcV2 = weightMode && (i % 6 == 1) && aqcV2Marked < MAX_AQC_V2_PARENTS; + if (markAqcV2) { + aqcV2Marked++; + } + emitChildren(conf, top, weightMode, label, true, markAqcV2); + } + } + + /** + * Emits capacities (and per-label capacities) for the children of + * {@code parent}, then recurses one level into sub-parents. + */ + private static void emitChildren(CapacitySchedulerConfiguration conf, Node parent, + boolean weightMode, String label, boolean topLevel, boolean markAqcV2) { + if (parent.children.isEmpty()) { + return; + } + conf.setQueues(new QueuePath(parent.path), names(parent.children)); + float[] caps = splitPercentages(parent.children.size()); + for (int c = 0; c < parent.children.size(); c++) { + Node child = parent.children.get(c); + QueuePath childPath = new QueuePath(child.path); + // The last sub-parent of a top-level weight subtree flips its children + // back to percentage mode; mode must stay uniform among siblings only. + boolean childrenWeightMode = weightMode + && !(topLevel && c == parent.children.size() - 1 && !child.children.isEmpty()); + if (weightMode) { + conf.setNonLabeledQueueWeight(childPath, (c % 4) + 1f); + } else { + conf.setCapacity(childPath, caps[c]); + conf.setMaximumCapacity(childPath, 100f); + } + if (label != null) { + conf.setAccessibleNodeLabels(childPath, of(label)); + conf.setCapacityByLabel(childPath, label, caps[c]); + } + decorate(conf, child, c); + if (child.managedParent) { + emitManagedParent(conf, childPath, label); + } else if (!child.children.isEmpty()) { + if (markAqcV2 && c == DIRECT_LEAVES) { + emitAqcV2Template(conf, childPath); + } + emitChildren(conf, child, childrenWeightMode, label, false, false); + } + } + } + + private static void emitManagedParent(CapacitySchedulerConfiguration conf, + QueuePath path, String label) { + conf.setAutoCreateChildQueueEnabled(path, true); + conf.setAutoCreatedLeafQueueConfigCapacity(path, 25f); + conf.setAutoCreatedLeafQueueConfigMaxCapacity(path, 100f); + if (label != null) { + conf.setAutoCreatedLeafQueueTemplateCapacityByLabel(path, label, 25f); + } + } + + private static void emitAqcV2Template(CapacitySchedulerConfiguration conf, + QueuePath path) { + conf.setAutoQueueCreationV2Enabled(path, true); + conf.set(CS_PREFIX + path.getFullPath() + + ".auto-queue-creation-v2.template.capacity", "2w"); + conf.set(CS_PREFIX + path.getFullPath() + + ".auto-queue-creation-v2.leaf-template.maximum-applications", "100"); + } + + /** Realistic per-queue extras on a deterministic subset of queues. */ + private static void decorate(CapacitySchedulerConfiguration conf, Node node, + int childIndex) { + QueuePath path = new QueuePath(node.path); + if (childIndex % 3 == 0) { + conf.setUserLimitFactor(path, 2.0f); + } + if (childIndex % 5 == 1) { + conf.setAcl(path, QueueACL.SUBMIT_APPLICATIONS, "benchuser benchgroup"); + conf.setAcl(path, QueueACL.ADMINISTER_QUEUE, "benchadmin"); + } + if (node.isLeaf() && childIndex % 4 == 2) { + conf.setOrderingPolicy(path, CapacitySchedulerConfiguration.FAIR_APP_ORDERING_POLICY); + } + } + + // --------------------------------------------------------------------- + // Absolute-mode emission + // --------------------------------------------------------------------- + + private static void emitAbsolute(CapacitySchedulerConfiguration conf, + QueuePath rootPath, List<Node> tops) { + // Root keeps its fixed 100 percent capacity; setting an absolute capacity + // on root is rejected by CapacitySchedulerConfiguration. + for (Node top : tops) { + emitAbsoluteSubtree(conf, top); + } + } + + /** Returns {memoryMb, vcores} configured for the subtree minimum. */ + private static long[] emitAbsoluteSubtree(CapacitySchedulerConfiguration conf, + Node node) { + QueuePath path = new QueuePath(node.path); + if (node.children.isEmpty()) { + conf.setCapacity(path, "[memory=1024,vcores=1]"); + conf.set(CS_PREFIX + node.path + ".maximum-capacity", + "[memory=2048,vcores=2]"); + if (node.managedParent) { + conf.setAutoCreateChildQueueEnabled(path, true); + } + return new long[]{1024L, 1L}; + } + conf.setQueues(path, names(node.children)); + long mem = 0; + long vcores = 0; + for (Node child : node.children) { + long[] used = emitAbsoluteSubtree(conf, child); + mem += used[0]; + vcores += used[1]; + } + conf.setCapacity(path, "[memory=" + mem + ",vcores=" + vcores + "]"); + conf.set(CS_PREFIX + node.path + ".maximum-capacity", + "[memory=" + (2 * mem) + ",vcores=" + (2 * vcores) + "]"); + return new long[]{mem, vcores}; + } + + // --------------------------------------------------------------------- + // Placement rules and helpers + // --------------------------------------------------------------------- + + private static void setPlacementRules(CapacitySchedulerConfiguration conf, + List<String> leafPaths) { + if (leafPaths.isEmpty()) { + return; + } + List<String> mappings = new ArrayList<>(); + mappings.add("u:benchuser1:" + leafPaths.get(0)); + mappings.add("u:benchuser2:" + leafPaths.get(leafPaths.size() / 2)); + mappings.add("g:benchgroup:" + leafPaths.get(leafPaths.size() - 1)); + if (leafPaths.size() > 3) { + mappings.add("u:%user:" + leafPaths.get(1)); + } + conf.set(CapacitySchedulerConfiguration.QUEUE_MAPPING, + String.join(",", mappings)); + } + + private static int collect(Node node, List<String> leafPaths) { + int count = 1; + if (node.isLeaf()) { + leafPaths.add(node.path); + } + for (Node child : node.children) { + count += collect(child, leafPaths); + } + return count; + } + + /** + * Picks two plain sibling leaves whose capacity keys can be swapped to + * produce a valid changed configuration. Prefers an unlabeled percentage + * subtree; falls back to any sub-parent, then to direct leaves. + */ + private static String[] pickMutationSiblings(List<Node> tops) { + for (int pass = 0; pass < 2; pass++) { + for (int i = 0; i < tops.size(); i += 2) { + if (pass == 0 && labelFor(i) != null) { + continue; + } + Node top = tops.get(i); + for (Node child : top.children) { + List<Node> leaves = new ArrayList<>(); + for (Node leaf : child.children) { + if (leaf.isLeaf()) { + leaves.add(leaf); + } + } + if (leaves.size() >= 2) { + return new String[]{leaves.get(0).path, leaves.get(1).path}; + } + } + List<Node> direct = new ArrayList<>(); + for (Node child : top.children) { + if (child.isLeaf()) { + direct.add(child); + } + } + if (direct.size() >= 2) { + return new String[]{direct.get(0).path, direct.get(1).path}; + } + } + } + return new String[]{null, null}; + } + + private static String labelFor(int topIndex) { + if (topIndex % 4 != 0) { + return null; + } + return (topIndex % 8 == 0) ? LABEL_BLUE : LABEL_SSD; + } + + private static String[] names(List<Node> nodes) { + String[] names = new String[nodes.size()]; + for (int i = 0; i < nodes.size(); i++) { + names[i] = nodes.get(i).name; + } + return names; + } + + /** + * Splits 100 percent into n two-decimal shares summing to exactly 100.00. + */ + private static float[] splitPercentages(int n) { + float[] out = new float[n]; + int base = 10000 / n; Review Comment: Nit: `generate(0)` (e.g. `-Dcs.bench.sizes=0`) fails here with `ArithmeticException: / by zero`. A `targetQueueCount > 0` check in `generate` would give a clearer error. ########## hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestCapacitySchedulerConfigBenchmark.java: ########## @@ -0,0 +1,234 @@ +/** + * 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.hadoop.yarn.server.resourcemanager.scheduler.capacity; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.test.GenericTestUtils; +import org.apache.hadoop.yarn.conf.YarnConfiguration; +import org.apache.hadoop.yarn.server.resourcemanager.MockRM; +import org.apache.hadoop.yarn.server.resourcemanager.NodeAttributeTestUtils; +import org.apache.hadoop.yarn.server.resourcemanager.RMContext; +import org.apache.hadoop.yarn.server.resourcemanager.scheduler.QueueMetrics; +import org.apache.hadoop.yarn.server.resourcemanager.scheduler.ResourceScheduler; +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Capacity Scheduler configuration benchmark: scheduler load, queue refresh + * and configuration validation on generated trees of + * {@link CSConfigBenchmarkGenerator}. + * + * <p>It only runs with {@code -DRunCapacitySchedulerConfigBenchmark=true}. + * Each size should run in its own JVM, and each fork is a separate Maven + * invocation, for example three forks of every size:</p> + * <pre> + * RM=hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager + * for fork in 1 2 3; do for size in 10 100 1000 5000; do + * ./mvnw -o test -pl $RM -Dtest=TestCapacitySchedulerConfigBenchmark \ + * -DRunCapacitySchedulerConfigBenchmark=true -Dcs.bench.sizes=$size \ + * -Dcs.bench.fork=$fork -Dcs.bench.out=/tmp/cs-bench.txt + * done; done + * </pre> + * <p>Properties:</p> + * <ul> + * <li>{@code cs.bench.sizes}: requested queue counts, default + * {@code 10,100,1000,5000};</li> + * <li>{@code cs.bench.warmups} (default 2) and {@code cs.bench.iterations} + * (default 5) per operation; the median of the iterations is reported;</li> + * <li>{@code cs.bench.ops}: subset of {@code scheduler-load}, + * {@code refresh} and {@code validate}, default all;</li> + * <li>{@code cs.bench.validators}: implementations measured by the + * {@code validate} operation, default {@code legacy} + * ({@link CapacitySchedulerConfigValidator#validateCSConfiguration}, the + * core of {@code POST /scheduler-conf/validate}); any other entry is the + * class name of a {@link ProposalValidator} with a no-argument constructor, + * which is the hook for {@code validate/v2};</li> + * <li>{@code cs.bench.capacityMode}: a + * {@link CSConfigBenchmarkGenerator.CapacityMode}, default + * {@code PCT_WEIGHT_MIXED};</li> + * <li>{@code cs.bench.fork}: label printed with the results;</li> + * <li>{@code cs.bench.out}: file the {@code BENCH} result lines are + * appended to (they are always printed to standard output).</li> + * </ul> + * <p>Operations: {@code scheduler-load} initializes a fresh + * {@link CapacityScheduler} from the configuration, {@code refresh} + * reinitializes the running scheduler with the unchanged configuration (what + * {@code AdminService.refreshQueues} does after loading the file), and + * {@code validate} validates a proposal that moves one percent of capacity + * between two sibling leaves.</p> + */ +public class TestCapacitySchedulerConfigBenchmark { + + /** A validation implementation measured by the {@code validate} op. */ + public interface ProposalValidator { + /** + * Validates {@code proposed} (the live configuration with the proposal + * applied) against the running scheduler, throwing when it is invalid. + */ + void validate(RMContext rmContext, Configuration live, + Configuration proposed) throws Exception; + } + + /** The legacy {@code POST /scheduler-conf/validate} core. */ + public static final class LegacyValidator implements ProposalValidator { + @Override + public void validate(RMContext rmContext, Configuration live, + Configuration proposed) throws IOException { + CapacitySchedulerConfigValidator.validateCSConfiguration(live, proposed, + rmContext); + } + } + + private interface Op { + void run() throws Exception; + } + + private final List<String> ops = Arrays.asList(System.getProperty( + "cs.bench.ops", "scheduler-load,refresh,validate").split(",")); + private final int warmups = Integer.getInteger("cs.bench.warmups", 2); + private final int iterations = Integer.getInteger("cs.bench.iterations", 5); + private final String fork = System.getProperty("cs.bench.fork", "1"); + + @Test + public void testConfigurationOperations() throws Exception { + assumeTrue(Boolean.getBoolean("RunCapacitySchedulerConfigBenchmark")); + for (String size : System.getProperty("cs.bench.sizes", "10,100,1000,5000") + .split(",")) { + runForSize(Integer.parseInt(size.trim())); + } + } + + private void runForSize(int requested) throws Exception { + CSConfigBenchmarkGenerator.GeneratedConfig generated = + CSConfigBenchmarkGenerator.generate(requested, + CSConfigBenchmarkGenerator.CapacityMode.valueOf(System.getProperty( + "cs.bench.capacityMode", "PCT_WEIGHT_MIXED"))); + Map<String, String> proposal = toMap( + CSConfigBenchmarkGenerator.createMutatedCopy(generated.getConf(), generated)); + YarnConfiguration conf = + NodeAttributeTestUtils.getRandomDirConf(generated.getConf()); + conf.setClass(YarnConfiguration.RM_SCHEDULER, CapacityScheduler.class, + ResourceScheduler.class); + + MockRM rm = new MockRM(conf); + try { + rm.start(); + // MockRM switches the root logger to DEBUG, which dominates the timings. + GenericTestUtils.setRootLogLevel(Level.WARN); + rm.registerNode("h1:1234", 100 * 1024, 100); + CapacityScheduler cs = (CapacityScheduler) rm.getResourceScheduler(); + RMContext rmContext = rm.getRMContext(); + int queues = cs.getCapacitySchedulerQueueManager().getQueues().size() - 1; + assertEquals(generated.getQueueCount(), queues); + + measure(requested, queues, "scheduler-load", () -> { + CapacityScheduler fresh = new CapacityScheduler(); + try { + fresh.setConf(cs.getConf()); + fresh.setRMContext(rmContext); + fresh.init(cs.getConf()); + assertEquals(queues + 1, + fresh.getCapacitySchedulerQueueManager().getQueues().size()); + } finally { + fresh.stop(); + } + }); + measure(requested, queues, "refresh", + () -> cs.reinitialize(cs.getConf(), rmContext)); + + Configuration live = cs.getConf(); + Configuration proposed = new Configuration(live); + proposal.forEach(proposed::set); + for (String name : System.getProperty("cs.bench.validators", "legacy") + .split(",")) { + ProposalValidator validator = name.equals("legacy") ? new LegacyValidator() + : (ProposalValidator) Class.forName(name.trim()).getDeclaredConstructor() + .newInstance(); + measure(requested, queues, "validate", name.trim(), + () -> validator.validate(rmContext, live, proposed)); + } + } finally { + rm.stop(); + QueueMetrics.clearQueueMetrics(); + GenericTestUtils.setRootLogLevel(Level.INFO); + } + } + + private void measure(int requested, int queues, String op, Op body) + throws Exception { + measure(requested, queues, op, null, body); + } + + private void measure(int requested, int queues, String op, String variant, + Op body) throws Exception { + if (!ops.contains(op)) { + return; + } + for (int i = 0; i < warmups; i++) { + body.run(); + } + double[] samples = new double[iterations]; + for (int i = 0; i < iterations; i++) { + long start = System.nanoTime(); + body.run(); + samples[i] = (System.nanoTime() - start) / 1e6; + } + double[] sorted = samples.clone(); + Arrays.sort(sorted); + List<String> all = new ArrayList<>(); + for (double sample : samples) { + all.add(String.format(Locale.ROOT, "%.1f", sample)); + } + String line = String.format(Locale.ROOT, + "BENCH fork=%s requested=%d queues=%d op=%s%s warmups=%d n=%d " + + "median_ms=%.1f all_ms=%s", + fork, requested, queues, op, variant == null ? "" : "-" + variant, + warmups, iterations, sorted[(iterations - 1) / 2], String.join(",", all)); Review Comment: `sorted[(iterations - 1) / 2]` is the lower middle sample when `cs.bench.iterations` is even, and throws `ArrayIndexOutOfBoundsException` for 0. Consider averaging the two middle samples for even counts and rejecting iterations < 1. ########## hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/CSConfigBenchmarkGenerator.java: ########## @@ -0,0 +1,543 @@ +/** + * 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.hadoop.yarn.server.resourcemanager.scheduler.capacity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.yarn.api.records.QueueACL; + +import static org.apache.hadoop.thirdparty.com.google.common.collect.ImmutableSet.of; + +/** + * Generates synthetic but structurally realistic capacity-scheduler + * configuration content for a requested queue count. + * It feeds {@link TestCapacitySchedulerConfigBenchmark} (scheduler load, + * refresh and validation timings) and {@link TestCSConfigBenchmarkGenerator}. + * + * Tree shape (mixed depth 3 and 4): + * <pre> + * root + * p0 (pct children, node label, one legacy managed parent leaf) + * p1 (weight children, AQC v2 template on sub-parent s0) + * p2 (pct children, unlabeled) + * p3 (weight children) + * ... + * each pI: 2 direct leaves + up to 5 sub-parents with up to 9 leaves each + * </pre> + * + * Capacity modes: + * - PCT_WEIGHT_MIXED (default): root children are percentage; even-indexed + * subtrees use percentage children, odd-indexed use weight children; the last + * sub-parent of a weight subtree flips its children back to percentage + * (legal per the legacy-mode parent/child mode table in AbstractParentQueue). + * - ALL_ABSOLUTE: every queue uses absolute [memory=..,vcores=..] resources, + * with parent minimums equal to the sum of their children. This is a separate + * mode because in legacy queue mode absolute resources cannot be mixed with + * percentage/weight anywhere in one hierarchy (absolute children require an + * absolute parent, and siblings must be mode-uniform). + * + * Extras sprinkled for realism: accessible-node-labels + per-label capacities + * on every fourth subtree (labels "blue"/"ssd"), legacy managed-parent + * (auto-create-child-queue) leaf templates, auto-queue-creation-v2 templates, + * legacy queue-mapping placement rules, ACLs, user-limit-factor, + * maximum-capacity and ordering policies on a subset of queues. + */ +public final class CSConfigBenchmarkGenerator { + + public enum CapacityMode { + PCT_WEIGHT_MIXED, + ALL_ABSOLUTE + } + + /** Everything the harness needs to know about a generated config. */ + public static final class GeneratedConfig { + private final CapacitySchedulerConfiguration conf; + private final int queueCount; + private final List<String> leafPaths; + private final String mutationLeafA; + private final String mutationLeafB; + private final List<String> labels; + + private GeneratedConfig(CapacitySchedulerConfiguration conf, int queueCount, + List<String> leafPaths, String mutationLeafA, String mutationLeafB, + List<String> labels) { + this.conf = conf; + this.queueCount = queueCount; + this.leafPaths = leafPaths; + this.mutationLeafA = mutationLeafA; + this.mutationLeafB = mutationLeafB; + this.labels = labels; + } + + public CapacitySchedulerConfiguration getConf() { + return conf; + } + + /** Number of generated queues, root excluded. */ + public int getQueueCount() { + return queueCount; + } + + public List<String> getLeafPaths() { + return Collections.unmodifiableList(leafPaths); + } + + /** Two sibling leaves whose capacities can be swapped for a valid change. */ + public String getMutationLeafA() { + return mutationLeafA; + } + + public String getMutationLeafB() { + return mutationLeafB; + } + + public List<String> getLabels() { + return Collections.unmodifiableList(labels); + } + } + + private static final int DIRECT_LEAVES = 2; + private static final int SUB_PARENTS = 5; + private static final int LEAVES_PER_SUB_PARENT = 9; + private static final int MAX_MANAGED_PARENTS = 4; + private static final int MAX_AQC_V2_PARENTS = 4; + private static final String LABEL_BLUE = "blue"; + private static final String LABEL_SSD = "ssd"; + private static final String CS_PREFIX = CapacitySchedulerConfiguration.PREFIX; + + private CSConfigBenchmarkGenerator() { + } + + private static final class Node { + private final String name; + private final String path; + private final List<Node> children = new ArrayList<>(); + private boolean managedParent; + + Node(String parentPath, String name) { + this.name = name; + this.path = parentPath + "." + name; + } + + boolean isLeaf() { + return children.isEmpty() && !managedParent; + } + } + + public static GeneratedConfig generate(int targetQueueCount) { + return generate(targetQueueCount, CapacityMode.PCT_WEIGHT_MIXED); + } + + public static GeneratedConfig generate(int targetQueueCount, CapacityMode mode) { + List<Node> tops = buildForest(targetQueueCount); + CapacitySchedulerConfiguration conf = + new CapacitySchedulerConfiguration(new Configuration(false), false); + + markManagedParents(tops); + QueuePath rootPath = new QueuePath(CapacitySchedulerConfiguration.ROOT); + conf.setQueues(rootPath, names(tops)); + + if (mode == CapacityMode.ALL_ABSOLUTE) { + emitAbsolute(conf, rootPath, tops); + } else { + emitMixed(conf, tops); + } + + List<String> leafPaths = new ArrayList<>(); + int count = 0; + for (Node top : tops) { + count += collect(top, leafPaths); + } + String[] mutationPair = pickMutationSiblings(tops); + setPlacementRules(conf, leafPaths); + + List<String> labels = new ArrayList<>(); + if (mode == CapacityMode.PCT_WEIGHT_MIXED) { + for (int i = 0; i < tops.size(); i++) { + String label = labelFor(i); + if (label != null && !labels.contains(label)) { + labels.add(label); + } + } + } + return new GeneratedConfig(conf, count, leafPaths, mutationPair[0], + mutationPair[1], labels); + } + + /** + * Returns a copy of {@code base} with a valid capacity change applied: one + * percent is moved from mutation leaf B to mutation leaf A (and likewise for + * their per-label capacities), so per-parent sums stay at 100. + */ + public static Configuration createMutatedCopy(Configuration base, + GeneratedConfig gen) { + Configuration copy = new Configuration(base); + moveOnePercent(copy, gen.getMutationLeafA(), gen.getMutationLeafB(), null); + for (String label : gen.getLabels()) { + moveOnePercent(copy, gen.getMutationLeafA(), gen.getMutationLeafB(), label); + } + return copy; + } + + private static void moveOnePercent(Configuration conf, String leafA, + String leafB, String label) { + if (leafA == null || leafB == null) { + return; + } + String suffix = (label == null) ? ".capacity" + : ".accessible-node-labels." + label + ".capacity"; + String keyA = CS_PREFIX + leafA + suffix; + String keyB = CS_PREFIX + leafB + suffix; + String a = conf.get(keyA); + String b = conf.get(keyB); + if (a == null || b == null) { + return; + } + try { + conf.set(keyA, String.valueOf(Float.parseFloat(a) + 1.0f)); + conf.set(keyB, String.valueOf(Float.parseFloat(b) - 1.0f)); + } catch (NumberFormatException e) { + // Absolute/weight profiles mutate a real queue limit, never a no-op. + conf.set(CS_PREFIX + leafA + ".maximum-applications", "2345"); + } + } + + // --------------------------------------------------------------------- + // Tree construction + // --------------------------------------------------------------------- + + private static List<Node> buildForest(int targetQueueCount) { + List<Node> tops = new ArrayList<>(); + int fullSubtree = 1 + DIRECT_LEAVES + SUB_PARENTS * (1 + LEAVES_PER_SUB_PARENT); + int remaining = targetQueueCount; + int i = 0; + while (remaining > 0) { + int budget = Math.min(fullSubtree, remaining); + Node top = new Node(CapacitySchedulerConfiguration.ROOT, "p" + i); + budget--; + int directLeaves = Math.min(DIRECT_LEAVES, budget); + for (int l = 0; l < directLeaves; l++) { + top.children.add(new Node(top.path, "l" + l)); + } + budget -= directLeaves; + int s = 0; + while (budget > 0) { + Node sub = new Node(top.path, "s" + s); + top.children.add(sub); + budget--; + int subLeaves = Math.min(LEAVES_PER_SUB_PARENT, budget); + for (int l = 0; l < subLeaves; l++) { + sub.children.add(new Node(sub.path, "l" + l)); + } + budget -= subLeaves; + s++; + } + remaining -= subtreeSize(top); + tops.add(top); + i++; + } + return tops; + } + + private static int subtreeSize(Node node) { + int size = 1; + for (Node child : node.children) { + size += subtreeSize(child); + } + return size; + } + + /** + * Converts the last direct leaf of every other percentage subtree into a + * legacy managed parent (auto-create-child-queue.enabled), capped globally. + */ + private static void markManagedParents(List<Node> tops) { + int marked = 0; + for (int i = 0; i < tops.size() && marked < MAX_MANAGED_PARENTS; i += 4) { + Node top = tops.get(i); + for (int c = top.children.size() - 1; c >= 0; c--) { + Node child = top.children.get(c); + if (child.isLeaf()) { + child.managedParent = true; + marked++; + break; + } + } + } + } + + // --------------------------------------------------------------------- + // Mixed percentage/weight emission + // --------------------------------------------------------------------- + + private static void emitMixed(CapacitySchedulerConfiguration conf, List<Node> tops) { + float[] topCaps = splitPercentages(tops.size()); + Map<String, List<Integer>> labelMembers = new LinkedHashMap<>(); + for (int i = 0; i < tops.size(); i++) { + String label = labelFor(i); + if (label != null) { + labelMembers.computeIfAbsent(label, k -> new ArrayList<>()).add(i); + } + } + // Per-label percentage shares among the labeled root children; the sums per + // label are 100 among label-accessible siblings, as legacy mode requires. + Map<Integer, Float> topLabelCap = new LinkedHashMap<>(); + for (List<Integer> members : labelMembers.values()) { + float[] shares = splitPercentages(members.size()); + for (int m = 0; m < members.size(); m++) { + topLabelCap.put(members.get(m), shares[m]); + } + } + + int aqcV2Marked = 0; + for (int i = 0; i < tops.size(); i++) { + Node top = tops.get(i); + QueuePath topPath = new QueuePath(top.path); + conf.setCapacity(topPath, topCaps[i]); + conf.setMaximumCapacity(topPath, 100f); + String label = labelFor(i); + if (label != null) { + conf.setAccessibleNodeLabels(topPath, of(label)); + conf.setCapacityByLabel(topPath, label, topLabelCap.get(i)); + } + boolean weightMode = (i % 2 == 1); + boolean markAqcV2 = weightMode && (i % 6 == 1) && aqcV2Marked < MAX_AQC_V2_PARENTS; + if (markAqcV2) { + aqcV2Marked++; + } + emitChildren(conf, top, weightMode, label, true, markAqcV2); + } + } + + /** + * Emits capacities (and per-label capacities) for the children of + * {@code parent}, then recurses one level into sub-parents. + */ + private static void emitChildren(CapacitySchedulerConfiguration conf, Node parent, + boolean weightMode, String label, boolean topLevel, boolean markAqcV2) { + if (parent.children.isEmpty()) { + return; + } + conf.setQueues(new QueuePath(parent.path), names(parent.children)); + float[] caps = splitPercentages(parent.children.size()); + for (int c = 0; c < parent.children.size(); c++) { + Node child = parent.children.get(c); + QueuePath childPath = new QueuePath(child.path); + // The last sub-parent of a top-level weight subtree flips its children + // back to percentage mode; mode must stay uniform among siblings only. + boolean childrenWeightMode = weightMode + && !(topLevel && c == parent.children.size() - 1 && !child.children.isEmpty()); + if (weightMode) { + conf.setNonLabeledQueueWeight(childPath, (c % 4) + 1f); + } else { + conf.setCapacity(childPath, caps[c]); + conf.setMaximumCapacity(childPath, 100f); + } + if (label != null) { + conf.setAccessibleNodeLabels(childPath, of(label)); + conf.setCapacityByLabel(childPath, label, caps[c]); + } + decorate(conf, child, c); + if (child.managedParent) { + emitManagedParent(conf, childPath, label); + } else if (!child.children.isEmpty()) { + if (markAqcV2 && c == DIRECT_LEAVES) { Review Comment: When a weight subtree has a single sub-parent (e.g. `generate(60)`, `root.p1`), `s0` is also the last sub-parent, so its children are flipped back to percentage, but it still gets the `2w` AQC v2 template. Creating a dynamic queue under it then fails: ``` SchedulerDynamicEditException: Trying to create new queue=root.p1.s0.dyn but not all the queues under parent=root.p1.s0 are using weight-based capacity ``` With `generate(200)`, `s0` has weight children and the same `createQueue` succeeds. Suggest `if (markAqcV2 && c == DIRECT_LEAVES && childrenWeightMode)`, and maybe an assertion in `TestCSConfigBenchmarkGenerator` that each AQC v2 parent can create a queue. ########## hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/test/java/org/apache/hadoop/yarn/server/resourcemanager/scheduler/capacity/TestAbsoluteResourceConfiguration.java: ########## @@ -637,6 +642,73 @@ public void testComplexValidateAbsoluteResourceConfig() throws Exception { rm.stop(); } + @Test + public void testQueueMixingAbsoluteAndPercentageAcrossLabelsRejected() { + CapacitySchedulerConfiguration csConf = new CapacitySchedulerConfiguration(); + csConf.setClass(YarnConfiguration.RM_SCHEDULER, CapacityScheduler.class, + ResourceScheduler.class); + csConf.setQueues(ROOT, new String[]{QUEUEA, QUEUEB}); + csConf.setCapacity(QUEUEA_FULL, 50f); + csConf.setCapacity(QUEUEB_FULL, 50f); + csConf.setAccessibleNodeLabels(QUEUEA_FULL, ImmutableSet.of(X_LABEL)); + csConf.setMinimumResourceRequirement(X_LABEL, QUEUEA_FULL, Resource.newInstance(GB, 1)); + + Exception e = assertThrows(Exception.class, () -> { + try (MockRM rm = new MockRM(csConf)) { + rm.start(); + } + }); + assertEquals("Queue 'root.queueA' should use either percentage based capacity " + + "configuration or absolute resource.", ExceptionUtils.getRootCause(e).getMessage()); + } + + @Test + public void testDefaultResourceCalculatorAcceptsChildVcoresAboveParent() + throws Exception { + CapacitySchedulerConfiguration csConf = new CapacitySchedulerConfiguration(); + csConf.setClass(YarnConfiguration.RM_SCHEDULER, CapacityScheduler.class, + ResourceScheduler.class); + csConf.setQueues(ROOT, new String[]{QUEUEA, QUEUEB}); + csConf.setQueues(QUEUEA_FULL, new String[]{QUEUEA1, QUEUEA2}); + csConf.setCapacity(QUEUEA_FULL, "[memory=40960,vcores=4]"); + csConf.setCapacity(QUEUEA1_FULL, "[memory=20480,vcores=8]"); + csConf.setCapacity(QUEUEA2_FULL, "[memory=20Gi,vcores=8]"); + csConf.setCapacity(QUEUEB_FULL, "[memory=40960,vcores=40]"); + + try (MockRM rm = new MockRM(csConf)) { + rm.start(); + CapacityScheduler cs = (CapacityScheduler) rm.getResourceScheduler(); + // Only memory is compared against the parent, so 16 vcores fit under 4. + assertEquals(Resource.newInstance(20 * GB, 8), cs.getQueue(QUEUEA1_FULL.getFullPath()) + .getQueueResourceQuotas().getConfiguredMinResource()); + assertEquals(Resource.newInstance(20 * GB, 8), cs.getQueue(QUEUEA2_FULL.getFullPath()) + .getQueueResourceQuotas().getConfiguredMinResource()); + } + } + + @Test + public void testDominantResourceCalculatorScalesOversubscribedQueues() + throws Exception { + CapacitySchedulerConfiguration csConf = new CapacitySchedulerConfiguration(); + csConf.setClass(YarnConfiguration.RM_SCHEDULER, CapacityScheduler.class, + ResourceScheduler.class); + csConf.setResourceComparator(DominantResourceCalculator.class); + csConf.setQueues(ROOT, new String[]{QUEUEA, QUEUEB}); + csConf.setCapacity(QUEUEA_FULL, "[memory=10240,vcores=60]"); + csConf.setCapacity(QUEUEB_FULL, "[memory=10240,vcores=60]"); + + try (MockRM rm = new MockRM(csConf)) { + rm.start(); + rm.registerNode("127.0.0.1:1234", 100 * GB, 100); + CapacityScheduler cs = (CapacityScheduler) rm.getResourceScheduler(); + // 120 configured vcores exceed the cluster, so both queues are scaled down. Review Comment: Nit: memory actually goes up here (10 GB configured, 50 GB effective) while vcores go down, so "scaled down" is a bit misleading. Maybe: "each queue gets half of the cluster in every resource". -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
