aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2020-10-08 13:37:45 +0200
committerMartin Polden <mpolden@mpolden.no>2020-10-16 09:33:00 +0200
commit408ee5ad7b66a93766ac52da062531a0972a5e97 (patch)
tree3f6fe5bf1a57e653a0d8b2e8204a821adf28e53f
parent76602902918085b557cdbcce537e27da12457a45 (diff)
Prefer nodes on exclusive switches
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java2
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidate.java48
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodePrioritizer.java33
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DynamicDockerAllocationTest.java92
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeCandidateTest.java54
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java20
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())