diff options
author | Martin Polden <mpolden@mpolden.no> | 2020-10-08 13:37:45 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2020-10-16 09:33:00 +0200 |
commit | 408ee5ad7b66a93766ac52da062531a0972a5e97 (patch) | |
tree | 3f6fe5bf1a57e653a0d8b2e8204a821adf28e53f | |
parent | 76602902918085b557cdbcce537e27da12457a45 (diff) |
Prefer nodes on exclusive switches
7 files changed, 202 insertions, 49 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java index ad85235fc69..3b41fe7874b 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java @@ -157,7 +157,7 @@ public class NodeList extends AbstractFilteringList<Node, NodeList> { } /** Returns the parent nodes of the given child nodes */ - public NodeList parentsOf(Collection<Node> children) { + public NodeList parentsOf(NodeList children) { return children.stream() .map(this::parentOf) .filter(Optional::isPresent) diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java index e92f039ad01..b9a337437e4 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java @@ -324,7 +324,7 @@ public class NodeRepository extends AbstractComponent { trustedNodes.addAll(candidates.nodeType(NodeType.config).asList()); trustedNodes.addAll(candidates.nodeType(NodeType.proxy).asList()); node.allocation().ifPresent(allocation -> - trustedNodes.addAll(candidates.parentsOf(candidates.owner(allocation.owner()).asList()).asList())); + trustedNodes.addAll(candidates.parentsOf(candidates.owner(allocation.owner())).asList())); if (node.state() == State.ready) { // Tenant nodes in state ready, trust: diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java index ef77b0397eb..1d60e60a1c9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java @@ -12,15 +12,12 @@ import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.Nodelike; import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.IP; -import com.yahoo.vespa.hosted.provision.node.Status; -import com.yahoo.vespa.hosted.provision.persistence.NameResolver; import com.yahoo.yolean.Exceptions; import java.time.Instant; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.logging.Level; import java.util.logging.Logger; /** @@ -42,12 +39,15 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { /** The free capacity on the parent of this node, before adding this node to it */ protected final NodeResources freeParentCapacity; - /** The parent host (docker or hypervisor) */ + /** The parent host */ final Optional<Node> parent; - /** True if the node is allocated to a host that should be dedicated as a spare */ + /** True if this node is allocated on a host that should be dedicated as a spare */ final boolean violatesSpares; + /** True if this node is allocated on an exclusive network switch in its cluster */ + final boolean exclusiveSwitch; + /** True if this node belongs to a group which will not be needed after this deployment */ final boolean isSurplus; @@ -57,13 +57,14 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { /** This node can be resized to the new NodeResources */ final boolean isResizable; - private NodeCandidate(NodeResources freeParentCapacity, Optional<Node> parent, boolean violatesSpares, boolean isSurplus, boolean isNew, boolean isResizeable) { + private NodeCandidate(NodeResources freeParentCapacity, Optional<Node> parent, boolean violatesSpares, boolean exclusiveSwitch, boolean isSurplus, boolean isNew, boolean isResizeable) { if (isResizeable && isNew) throw new IllegalArgumentException("A new node cannot be resizable"); this.freeParentCapacity = freeParentCapacity; this.parent = parent; this.violatesSpares = violatesSpares; + this.exclusiveSwitch = exclusiveSwitch; this.isSurplus = isSurplus; this.isNew = isNew; this.isResizable = isResizeable; @@ -91,7 +92,7 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { /** * Compare this candidate to another * - * @return negative if first priority is higher than second node + * @return negative if this should be preferred over other */ @Override public int compareTo(NodeCandidate other) { @@ -107,7 +108,11 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { if (!this.isSurplus && other.isSurplus) return -1; if (!other.isSurplus && this.isSurplus) return 1; - // Choose reserved nodes from a previous allocation attempt (the exist in node repo) + // Prefer node on exclusive switch + if (this.exclusiveSwitch && !other.exclusiveSwitch) return -1; + if (other.exclusiveSwitch && !this.exclusiveSwitch) return 1; + + // Choose reserved nodes from a previous allocation attempt (which exist in node repo) if (this.isInNodeRepoAndReserved() && ! other.isInNodeRepoAndReserved()) return -1; if (other.isInNodeRepoAndReserved() && ! this.isInNodeRepoAndReserved()) return 1; @@ -172,7 +177,7 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { /** Returns a copy of this with node set to given value */ NodeCandidate withNode(Node node) { - return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, isSurplus, isNew, isResizable); + return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable); } private boolean lessThanHalfTheHost(NodeCandidate node) { @@ -200,27 +205,29 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { NodeResources freeParentCapacity, Node parent, boolean violatesSpares, + boolean exclusiveSwitch, boolean isSurplus, boolean isNew, boolean isResizeable) { - return new ConcreteNodeCandidate(node, freeParentCapacity, Optional.of(parent), violatesSpares, isSurplus, isNew, isResizeable); + return new ConcreteNodeCandidate(node, freeParentCapacity, Optional.of(parent), violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizeable); } public static NodeCandidate createNewChild(NodeResources resources, NodeResources freeParentCapacity, Node parent, boolean violatesSpares, + boolean exclusiveSwitch, LockedNodeList allNodes, NodeRepository nodeRepository) { - return new VirtualNodeCandidate(resources, freeParentCapacity, parent, violatesSpares, allNodes, nodeRepository); + return new VirtualNodeCandidate(resources, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, allNodes, nodeRepository); } public static NodeCandidate createNewExclusiveChild(Node node, Node parent) { - return new ConcreteNodeCandidate(node, node.resources(), Optional.of(parent), false, false, true, false); + return new ConcreteNodeCandidate(node, node.resources(), Optional.of(parent), false, true, false, true, false); } - public static NodeCandidate createStandalone(Node node, boolean isSurplus, boolean isNew) { - return new ConcreteNodeCandidate(node, node.resources(), Optional.empty(), false, isSurplus, isNew, false); + public static NodeCandidate createStandalone(Node node, boolean isSurplus, boolean isNew, boolean exclusiveSwitch) { + return new ConcreteNodeCandidate(node, node.resources(), Optional.empty(), false, exclusiveSwitch, isSurplus, isNew, false); } /** A candidate backed by a node */ @@ -229,9 +236,9 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { private final Node node; ConcreteNodeCandidate(Node node, NodeResources freeParentCapacity, Optional<Node> parent, - boolean violatesSpares, + boolean violatesSpares, boolean exclusiveSwitch, boolean isSurplus, boolean isNew, boolean isResizeable) { - super(freeParentCapacity, parent, violatesSpares, isSurplus, isNew, isResizeable); + super(freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizeable); this.node = Objects.requireNonNull(node, "Node cannot be null"); } @@ -259,7 +266,7 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { @Override public NodeCandidate allocate(ApplicationId owner, ClusterMembership membership, NodeResources requestedResources, Instant at) { return new ConcreteNodeCandidate(node.allocate(owner, membership, requestedResources, at), - freeParentCapacity, parent, violatesSpares, isSurplus, isNew, isResizable); + freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable); } /** Called when the node described by this candidate must be created */ @@ -303,9 +310,10 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { NodeResources freeParentCapacity, Node parent, boolean violatesSpares, + boolean exclusiveSwitch, LockedNodeList allNodes, NodeRepository nodeRepository) { - super(freeParentCapacity, Optional.of(parent), violatesSpares, false, true, false); + super(freeParentCapacity, Optional.of(parent), violatesSpares, exclusiveSwitch, false, true, false); this.resources = resources; this.allNodes = allNodes; this.nodeRepository = nodeRepository; @@ -354,7 +362,7 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { resources.with(parent.get().resources().diskSpeed()) .with(parent.get().resources().storageType()), NodeType.tenant); - return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, isSurplus, isNew, isResizable); + return new ConcreteNodeCandidate(node, freeParentCapacity, parent, violatesSpares, exclusiveSwitch, isSurplus, isNew, isResizable); } @@ -390,7 +398,7 @@ abstract class NodeCandidate implements Nodelike, Comparable<NodeCandidate> { private final NodeResources resources; private InvalidNodeCandidate(NodeResources resources, NodeResources freeParentCapacity, Node parent) { - super(freeParentCapacity, Optional.of(parent), false, false, true, false); + super(freeParentCapacity, Optional.of(parent), false, false, false, true, false); this.resources = resources; } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java index b6b05949082..d2163fc9b55 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java @@ -123,6 +123,7 @@ public class NodePrioritizer { capacity.freeCapacityOf(host, false), host, spareHosts.contains(host), + unusedSwitch(host, clusterSpec.id()), allNodes, nodeRepository)); } @@ -138,7 +139,7 @@ public class NodePrioritizer { .filter(node -> node.allocation().get().owner().equals(application)) .filter(node -> node.state() == Node.State.active || canStillAllocateToParentOf(node)) .map(node -> candidateFrom(node, false)) - .forEach(candidate -> nodes.add(candidate)); + .forEach(nodes::add); } /** Add nodes already provisioned, but not allocated to any application */ @@ -148,17 +149,19 @@ public class NodePrioritizer { .filter(node -> node.state() == Node.State.ready) .map(node -> candidateFrom(node, false)) .filter(n -> !n.violatesSpares || isAllocatingForReplacement) - .forEach(candidate -> nodes.add(candidate)); + .forEach(nodes::add); } /** Create a candidate from given pre-existing node */ private NodeCandidate candidateFrom(Node node, boolean isSurplus) { Optional<Node> parent = allNodes.parentOf(node); + boolean exclusiveSwitch = onExclusiveSwitch(node, parent); if (parent.isPresent()) { return NodeCandidate.createChild(node, capacity.freeCapacityOf(parent.get(), false), parent.get(), spareHosts.contains(parent.get()), + exclusiveSwitch, isSurplus, false, !allocateFully @@ -168,10 +171,34 @@ public class NodePrioritizer { currentClusterSize)); } else { - return NodeCandidate.createStandalone(node, isSurplus, false); + return NodeCandidate.createStandalone(node, isSurplus, false, exclusiveSwitch); } } + /** Returns whether given node is on an exclusive switch */ + private boolean onExclusiveSwitch(Node node, Optional<Node> parent) { + Node host = parent.orElse(node); + return unusedSwitch(host, node.allocation() + .map(allocation -> allocation.membership().cluster().id()) + .orElseGet(clusterSpec::id)); + } + + /** Returns whether switch of host is unused by any existing candidates for given cluster */ + private boolean unusedSwitch(Node host, ClusterSpec.Id cluster) { + if (host.switchHostname().isEmpty()) return true; + String switchHostname = host.switchHostname().get(); + for (var candidate : nodes) { + ClusterSpec.Id candidateCluster = candidate.toNode().allocation() + .map(a -> a.membership().cluster().id()) + .orElseGet(clusterSpec::id); + if (!cluster.equals(candidateCluster)) continue; // Wrong cluster + Node node = candidate.parent.orElseGet(candidate::toNode); + if (node.switchHostname().isEmpty()) continue; + if (node.switchHostname().get().equals(switchHostname)) return false; + } + return true; + } + private boolean isReplacement(int nofNodesInCluster, int nodeFailedNodes) { if (nodeFailedNodes == 0) return false; diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java index 89072223341..98ea25e177f 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java @@ -20,6 +20,7 @@ import com.yahoo.config.provisioning.FlavorsConfig; import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.curator.transaction.CuratorTransaction; import com.yahoo.vespa.hosted.provision.Node; +import com.yahoo.vespa.hosted.provision.Node.State; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.IP; @@ -67,7 +68,7 @@ public class DynamicDockerAllocationTest { .build(); tester.makeReadyNodes(4, "host-small", NodeType.host, 32); tester.activateTenantHosts(); - List<Node> dockerHosts = tester.nodeRepository().getNodes(NodeType.host, Node.State.active); + List<Node> dockerHosts = tester.nodeRepository().getNodes(NodeType.host, State.active); NodeResources flavor = new NodeResources(1, 4, 100, 1); // Application 1 @@ -88,7 +89,7 @@ public class DynamicDockerAllocationTest { // Assert that we have two spare nodes (two hosts that are don't have allocations) Set<String> hostsWithChildren = new HashSet<>(); - for (Node node : tester.nodeRepository().getNodes(NodeType.tenant, Node.State.active)) { + for (Node node : tester.nodeRepository().getNodes(NodeType.tenant, State.active)) { if (!isInactiveOrRetired(node)) { hostsWithChildren.add(node.parentHostname().get()); } @@ -110,7 +111,7 @@ public class DynamicDockerAllocationTest { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(flavorsConfig()).build(); tester.makeReadyNodes(5, "host-small", NodeType.host, 32); tester.activateTenantHosts(); - List<Node> dockerHosts = tester.nodeRepository().getNodes(NodeType.host, Node.State.active); + List<Node> dockerHosts = tester.nodeRepository().getNodes(NodeType.host, State.active); NodeResources resources = new NodeResources(1, 4, 100, 0.3); // Application 1 @@ -202,7 +203,7 @@ public class DynamicDockerAllocationTest { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(flavorsConfig()).build(); tester.makeReadyNodes(2, "host-small", NodeType.host, 32); tester.activateTenantHosts(); - List<Node> dockerHosts = tester.nodeRepository().getNodes(NodeType.host, Node.State.active); + List<Node> dockerHosts = tester.nodeRepository().getNodes(NodeType.host, State.active); NodeResources flavor = new NodeResources(1, 4, 100, 1); // Application 1 @@ -216,7 +217,7 @@ public class DynamicDockerAllocationTest { // Assert that we have two spare nodes (two hosts that are don't have allocations) Set<String> hostsWithChildren = new HashSet<>(); - for (Node node : tester.nodeRepository().getNodes(NodeType.tenant, Node.State.active)) { + for (Node node : tester.nodeRepository().getNodes(NodeType.tenant, State.active)) { if (!isInactiveOrRetired(node)) { hostsWithChildren.add(node.parentHostname().get()); } @@ -301,7 +302,7 @@ public class DynamicDockerAllocationTest { public void allocation_should_fail_when_host_is_not_in_allocatable_state() { ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(flavorsConfig()).build(); tester.makeProvisionedNodes(3, "host-small", NodeType.host, 32).forEach(node -> - tester.nodeRepository().fail(node.hostname(), Agent.system, getClass().getSimpleName())); + tester.nodeRepository().fail(node.hostname(), Agent.system, getClass().getSimpleName())); ApplicationId application = ProvisioningTester.makeApplicationId(); tester.prepare(application, clusterSpec("myContent.t2.a2"), 2, 1, new NodeResources(1, 40, 100, 1)); @@ -414,6 +415,81 @@ public class DynamicDockerAllocationTest { assertEquals(hosts1, hosts2); } + @Test + public void testPreferExclusiveNetworkSwitch() { + // Hosts are provisioned, without switch information + ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))).flavorsConfig(flavorsConfig()).build(); + NodeResources hostResources = new NodeResources(32, 128, 2000, 10); + List<Node> hosts0 = tester.makeReadyNodes(3, hostResources, NodeType.host, 5); + tester.activateTenantHosts(); + + // Application is deployed + ClusterSpec cluster = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("test")).vespaVersion("1").build(); + NodeResources resources = new NodeResources(2, 4, 50, 1); + ApplicationId app1 = ApplicationId.from("t1", "a1", "i1"); + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(2, 1, resources)))); + tester.assertSwitches(Set.of(), app1, cluster.id()); + + // One host is provisioned on a known switch + String switch0 = "switch0"; + List<Node> hosts1 = tester.makeReadyNodes(1, hostResources, NodeType.host, 5); + tester.activateTenantHosts(); + tester.patchNodes(hosts1, (host) -> host.withSwitchHostname(switch0)); + + // Redeploy does not change allocation as a host with switch information is no better or worse than hosts + // without switch information + NodeList allocatedNodes = tester.nodeRepository().list().owner(app1); + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(2, 1, resources)))); + assertEquals("Allocation unchanged", allocatedNodes.asList(), tester.nodeRepository().getNodes(app1)); + + // Initial hosts are attached to the same switch + tester.patchNodes(hosts0, (host) -> host.withSwitchHostname(switch0)); + + // Redeploy does not change allocation + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(2, 1, resources)))); + assertEquals("Allocation unchanged", allocatedNodes.asList(), tester.nodeRepository().getNodes(app1)); + + // Additional hosts are provisioned on distinct switches + List<Node> hosts2 = tester.makeReadyNodes(2, hostResources, NodeType.host, 5); + tester.activateTenantHosts(); + for (var i = 0; i < hosts2.size(); i++) { + String switchHostname = "switch" + (i + 1); + tester.patchNode(hosts2.get(i), (host) -> host.withSwitchHostname(switchHostname)); + } + + // Redeploy does not change allocation as we prefer to keep our already active nodes + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(2, 1, resources)))); + tester.assertSwitches(Set.of(switch0), app1, cluster.id()); + + // A node is retired + tester.patchNode(tester.nodeRepository().list().owner(app1).asList().get(0), + (node) -> node.withWantToRetire(true, Agent.system, tester.clock().instant())); + + // Redeploy allocates new node on a distinct switch + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(2, 1, resources)))); + String switch2 = "switch2"; + tester.assertSwitches(Set.of(switch0, switch2), app1, cluster.id()); + + // Growing cluster picks new node on exclusive switch + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(3, 1, resources)))); + String switch1 = "switch1"; + tester.assertSwitches(Set.of(switch0, switch1, switch2), app1, cluster.id()); + + // Growing cluster further can reuse switches as we're now out of exclusive ones + tester.activate(app1, tester.prepare(app1, cluster, Capacity.from(new ClusterResources(4, 1, resources)))); + tester.assertSwitches(Set.of(switch0, switch1, switch2), app1, cluster.id()); + + // Additional cluster can reuse switches of existing cluster + ClusterSpec cluster2 = ClusterSpec.request(ClusterSpec.Type.content, ClusterSpec.Id.from("content")).vespaVersion("1").build(); + tester.activate(app1, tester.prepare(app1, cluster2, Capacity.from(new ClusterResources(3, 1, resources)))); + tester.assertSwitches(Set.of(switch0, switch1, switch2), app1, cluster2.id()); + + // Another application is deployed on exclusive switches + ApplicationId app2 = ApplicationId.from("t2", "a2", "i2"); + tester.activate(app2, tester.prepare(app2, cluster, Capacity.from(new ClusterResources(3, 1, resources)))); + tester.assertSwitches(Set.of(switch0, switch1, switch2), app2, cluster.id()); + } + private ApplicationId makeApplicationId(String tenant, String appName) { return ApplicationId.from(tenant, appName, "default"); } @@ -438,7 +514,7 @@ public class DynamicDockerAllocationTest { } private List<Node> findSpareCapacity(ProvisioningTester tester) { - List<Node> nodes = tester.nodeRepository().getNodes(Node.State.values()); + List<Node> nodes = tester.nodeRepository().getNodes(State.values()); NodeList nl = NodeList.copyOf(nodes); return nodes.stream() .filter(n -> n.type() == NodeType.host) @@ -457,7 +533,7 @@ public class DynamicDockerAllocationTest { } private boolean isInactiveOrRetired(Node node) { - boolean isInactive = node.state().equals(Node.State.inactive); + boolean isInactive = node.state().equals(State.inactive); boolean isRetired = false; if (node.allocation().isPresent()) { isRetired = node.allocation().get().membership().retired(); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java index cff5e1f46cf..60a4a3d1c3d 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java @@ -25,19 +25,19 @@ import static org.junit.Assert.assertEquals; public class NodeCandidateTest { @Test - public void test_order() { + public void testOrdering() { List<NodeCandidate> expected = List.of( - new NodeCandidate.ConcreteNodeCandidate(node("01", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), false, true, false, false), - new NodeCandidate.ConcreteNodeCandidate(node("02", Node.State.active), new NodeResources(2, 2, 2, 2), Optional.empty(), true, false, false, false), - new NodeCandidate.ConcreteNodeCandidate(node("04", Node.State.reserved), new NodeResources(2, 2, 2, 2), Optional.empty(), true, false, false, false), - new NodeCandidate.ConcreteNodeCandidate(node("03", Node.State.inactive), new NodeResources(2, 2, 2, 2), Optional.empty(), true, false, false, false), - new NodeCandidate.ConcreteNodeCandidate(node("05", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.active)), true, false, true, false), - new NodeCandidate.ConcreteNodeCandidate(node("06", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.ready)), true, false, true, false), - new NodeCandidate.ConcreteNodeCandidate(node("07", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.provisioned)), true, false, true, false), - new NodeCandidate.ConcreteNodeCandidate(node("08", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.failed)), true, false, true, false), - new NodeCandidate.ConcreteNodeCandidate(node("09", Node.State.ready), new NodeResources(1, 1, 1, 1), Optional.empty(), true, false, true, false), - new NodeCandidate.ConcreteNodeCandidate(node("10", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), true, false, true, false), - new NodeCandidate.ConcreteNodeCandidate(node("11", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), true, false, true, false) + new NodeCandidate.ConcreteNodeCandidate(node("01", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), false, true, true, false, false), + new NodeCandidate.ConcreteNodeCandidate(node("02", Node.State.active), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false), + new NodeCandidate.ConcreteNodeCandidate(node("04", Node.State.reserved), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false), + new NodeCandidate.ConcreteNodeCandidate(node("03", Node.State.inactive), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, false, false), + new NodeCandidate.ConcreteNodeCandidate(node("05", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.active)), true, true, false, true, false), + new NodeCandidate.ConcreteNodeCandidate(node("06", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.ready)), true, true, false, true, false), + new NodeCandidate.ConcreteNodeCandidate(node("07", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.provisioned)), true, true, false, true, false), + new NodeCandidate.ConcreteNodeCandidate(node("08", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.of(node("host1", Node.State.failed)), true, true, false, true, false), + new NodeCandidate.ConcreteNodeCandidate(node("09", Node.State.ready), new NodeResources(1, 1, 1, 1), Optional.empty(), true, true, false, true, false), + new NodeCandidate.ConcreteNodeCandidate(node("10", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, true, false), + new NodeCandidate.ConcreteNodeCandidate(node("11", Node.State.ready), new NodeResources(2, 2, 2, 2), Optional.empty(), true, true, false, true, false) ); assertOrder(expected); } @@ -108,6 +108,18 @@ public class NodeCandidateTest { assertOrder(expected); } + @Test + public void testOrderingByExclusiveSwitch() { + List<NodeCandidate> expected = List.of( + node("1", true), + node("2", true), + node("3", false), + node("4", false), + node("5", false) + ); + assertOrder(expected); + } + private void assertOrder(List<NodeCandidate> expected) { List<NodeCandidate> copy = new ArrayList<>(expected); Collections.shuffle(copy); @@ -133,7 +145,8 @@ public class NodeCandidateTest { private static NodeCandidate node(String hostname, NodeResources nodeResources, NodeResources allocatedHostResources, // allocated before adding nodeResources - NodeResources totalHostResources) { + NodeResources totalHostResources, + boolean exclusiveSwitch) { Node node = new Node(hostname, new IP.Config(Set.of("::1"), Set.of()), hostname, Optional.of(hostname + "parent"), new Flavor(nodeResources), Status.initial(), Node.State.ready, Optional.empty(), History.empty(), NodeType.tenant, @@ -143,7 +156,20 @@ public class NodeCandidateTest { Status.initial(), Node.State.ready, Optional.empty(), History.empty(), NodeType.host, new Reports(), Optional.empty(), Optional.empty(), Optional.empty()); return new NodeCandidate.ConcreteNodeCandidate(node, totalHostResources.subtract(allocatedHostResources), Optional.of(parent), - false, false, true, false); + false, exclusiveSwitch, false, true, false); + } + + private static NodeCandidate node(String hostname, NodeResources nodeResources, + NodeResources allocatedHostResources, NodeResources totalHostResources) { + return node(hostname, nodeResources, allocatedHostResources, totalHostResources, false); + } + + private static NodeCandidate node(String hostname, boolean exclusiveSwitch) { + return node(hostname, + node(2, 10), + host(20, 20), + host(40, 40), + exclusiveSwitch); } } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index 6b7b979e083..fbda776bd3a 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -171,9 +171,8 @@ public class ProvisioningTester { Set<String> reservedBefore = toHostNames(nodeRepository.getNodes(application, Node.State.reserved)); Set<String> inactiveBefore = toHostNames(nodeRepository.getNodes(application, Node.State.inactive)); List<HostSpec> hosts1 = provisioner.prepare(application, cluster, capacity, provisionLogger); - // prepare twice to ensure idempotence List<HostSpec> hosts2 = provisioner.prepare(application, cluster, capacity, provisionLogger); - assertEquals(hosts1, hosts2); + assertEquals("Prepare is idempotent", hosts1, hosts2); Set<String> newlyActivated = toHostNames(nodeRepository.getNodes(application, Node.State.reserved)); newlyActivated.removeAll(reservedBefore); newlyActivated.removeAll(inactiveBefore); @@ -563,6 +562,23 @@ public class ProvisioningTester { } } + public void assertSwitches(Set<String> expectedSwitches, ApplicationId application, ClusterSpec.Id cluster) { + NodeList allNodes = nodeRepository.list(); + NodeList activeNodes = allNodes.owner(application).state(Node.State.active).cluster(cluster); + assertEquals(expectedSwitches, switchesOf(activeNodes, allNodes)); + } + + private Set<String> switchesOf(NodeList applicationNodes, NodeList allNodes) { + assertTrue("All application nodes are children", applicationNodes.stream().allMatch(node -> node.parentHostname().isPresent())); + Set<String> switches = new HashSet<>(); + for (var parent : allNodes.parentsOf(applicationNodes)) { + Optional<String> switchHostname = parent.switchHostname(); + if (switchHostname.isEmpty()) continue; + switches.add(switchHostname.get()); + } + return switches; + } + public int hostFlavorCount(String hostFlavor, ApplicationId app) { return (int)nodeRepository().getNodes(app).stream() .map(n -> nodeRepository().getNode(n.parentHostname().get()).get()) |