summaryrefslogtreecommitdiffstats
path: root/node-repository
diff options
context:
space:
mode:
authorvalerijf <valerijf@yahoo-inc.com>2017-06-08 15:35:04 +0200
committervalerijf <valerijf@yahoo-inc.com>2017-06-08 15:35:04 +0200
commit4c05f765f772674fc248e8337558616da3cf7326 (patch)
tree057a8b5e86bcdce23241629727778c5358aa4cc9 /node-repository
parentf1665f30a371776124225a50a80907fdb24e0469 (diff)
Update NodeRetirer to use FlavorSpareChecker
Diffstat (limited to 'node-repository')
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java9
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirer.java114
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClusters.java60
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTest.java429
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTester.java90
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClustersTest.java108
6 files changed, 216 insertions, 594 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java
index d35a9c158b7..da3431b3d3a 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java
@@ -13,7 +13,8 @@ import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.curator.Curator;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.maintenance.retire.RetireIPv4OnlyNodes;
-import com.yahoo.vespa.hosted.provision.provisioning.FlavorClusters;
+import com.yahoo.vespa.hosted.provision.provisioning.FlavorSpareChecker;
+import com.yahoo.vespa.hosted.provision.provisioning.FlavorSpareCount;
import com.yahoo.vespa.orchestrator.Orchestrator;
import com.yahoo.vespa.service.monitor.ServiceMonitor;
@@ -61,6 +62,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent {
Zone zone, Clock clock, Orchestrator orchestrator, Metric metric) {
DefaultTimes defaults = new DefaultTimes(zone.environment());
jobControl = new JobControl(nodeRepository.database());
+
nodeFailer = new NodeFailer(deployer, hostLivenessTracker, serviceMonitor, nodeRepository, durationFromEnv("fail_grace").orElse(defaults.failGrace), clock, orchestrator, throttlePolicyFromEnv("throttle_policy").orElse(defaults.throttlePolicy), jobControl);
periodicApplicationMaintainer = new PeriodicApplicationMaintainer(deployer, nodeRepository, durationFromEnv("periodic_redeploy_interval").orElse(defaults.periodicRedeployInterval), jobControl);
operatorChangeApplicationMaintainer = new OperatorChangeApplicationMaintainer(deployer, nodeRepository, clock, durationFromEnv("operator_change_redeploy_interval").orElse(defaults.operatorChangeRedeployInterval), jobControl);
@@ -74,8 +76,9 @@ public class NodeRepositoryMaintenance extends AbstractComponent {
nodeRebooter = new NodeRebooter(nodeRepository, clock, durationFromEnv("reboot_interval").orElse(defaults.rebootInterval), jobControl);
metricsReporter = new MetricsReporter(nodeRepository, metric, durationFromEnv("metrics_interval").orElse(defaults.metricsInterval), jobControl);
- FlavorClusters flavorClusters = new FlavorClusters(zone.nodeFlavors().get().getFlavors());
- nodeRetirer = new NodeRetirer(nodeRepository, zone, flavorClusters, durationFromEnv("retire_interval").orElse(defaults.nodeRetirerInterval), jobControl,
+ FlavorSpareChecker flavorSpareChecker = new FlavorSpareChecker(
+ NodeRetirer.SPARE_NODES_POLICY, FlavorSpareCount.constructFlavorSpareCountGraph(zone.nodeFlavors().get().getFlavors()));
+ nodeRetirer = new NodeRetirer(nodeRepository, zone, flavorSpareChecker, durationFromEnv("retire_interval").orElse(defaults.nodeRetirerInterval), jobControl,
new RetireIPv4OnlyNodes(),
new Zone(SystemName.cd, Environment.dev, RegionName.from("cd-us-central-1")),
new Zone(SystemName.cd, Environment.prod, RegionName.from("cd-us-central-1")),
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirer.java
index 49184dab0af..c320fbcaff8 100644
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirer.java
+++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirer.java
@@ -9,11 +9,10 @@ import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.maintenance.retire.RetirementPolicy;
import com.yahoo.vespa.hosted.provision.node.Agent;
-import com.yahoo.vespa.hosted.provision.provisioning.FlavorClusters;
+import com.yahoo.vespa.hosted.provision.provisioning.FlavorSpareChecker;
import java.time.Duration;
import java.util.Arrays;
-import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -25,13 +24,16 @@ import java.util.stream.Collectors;
* @author freva
*/
public class NodeRetirer extends Maintainer {
+ public static final FlavorSpareChecker.SpareNodesPolicy SPARE_NODES_POLICY = flavorSpareCount ->
+ flavorSpareCount.getSumOfReadyAmongReplacees() > 2;
+
private static final long MAX_SIMULTANEOUS_RETIRES_PER_APPLICATION = 1;
private static final Logger log = Logger.getLogger(NodeRetirer.class.getName());
- private final FlavorClusters flavorClusters;
+ private final FlavorSpareChecker flavorSpareChecker;
private final RetirementPolicy retirementPolicy;
- public NodeRetirer(NodeRepository nodeRepository, Zone zone, FlavorClusters flavorClusters, Duration interval,
+ public NodeRetirer(NodeRepository nodeRepository, Zone zone, FlavorSpareChecker flavorSpareChecker, Duration interval,
JobControl jobControl, RetirementPolicy retirementPolicy, Zone... applies) {
super(nodeRepository, interval, jobControl);
if (! Arrays.asList(applies).contains(zone)) {
@@ -41,7 +43,7 @@ public class NodeRetirer extends Maintainer {
}
this.retirementPolicy = retirementPolicy;
- this.flavorClusters = flavorClusters;
+ this.flavorSpareChecker = flavorSpareChecker;
}
@Override
@@ -58,7 +60,8 @@ public class NodeRetirer extends Maintainer {
boolean retireUnallocated() {
try (Mutex lock = nodeRepository().lockUnallocated()) {
List<Node> allNodes = nodeRepository().getNodes(NodeType.tenant);
- Map<Flavor, Long> numSpareNodesByFlavor = getNumberSpareReadyNodesByFlavor(allNodes);
+ Map<Flavor, Map<Node.State, Long>> numNodesByFlavorByState = getNumberOfNodesByFlavorByNodeState(allNodes);
+ flavorSpareChecker.updateReadyAndActiveCountsByFlavor(numNodesByFlavorByState);
long numFlavorsWithUnsuccessfullyRetiredNodes = allNodes.stream()
.filter(node -> node.state() == Node.State.ready)
@@ -69,16 +72,23 @@ public class NodeRetirer extends Maintainer {
.entrySet().stream()
.filter(entry -> {
Set<Node> nodesThatShouldBeRetiredForFlavor = entry.getValue();
- long numSpareReadyNodesForFlavor = numSpareNodesByFlavor.get(entry.getKey());
- boolean parkedAll = limitedPark(nodesThatShouldBeRetiredForFlavor, numSpareReadyNodesForFlavor);
- if (!parkedAll) {
+ for (Iterator<Node> iter = nodesThatShouldBeRetiredForFlavor.iterator(); iter.hasNext(); ) {
+ Node nodeToRetire = iter.next();
+ if (! flavorSpareChecker.canRetireUnallocatedNodeWithFlavor(nodeToRetire.flavor())) break;
+
+ nodeRepository().write(nodeToRetire.with(nodeToRetire.status().withWantToDeprovision(true)));
+ nodeRepository().park(nodeToRetire.hostname(), Agent.NodeRetirer,
+ "Policy: " + retirementPolicy.getClass().getSimpleName());
+ iter.remove();
+ }
+
+ if (! nodesThatShouldBeRetiredForFlavor.isEmpty()) {
String commaSeparatedHostnames = nodesThatShouldBeRetiredForFlavor.stream().map(Node::hostname)
.collect(Collectors.joining(", "));
- log.info(String.format("Failed to retire %s, wanted to retire %d nodes (%s), but only %d spare " +
- "nodes available for flavor cluster.",
- entry.getKey(), nodesThatShouldBeRetiredForFlavor.size(), commaSeparatedHostnames, numSpareReadyNodesForFlavor));
+ log.info(String.format("Failed to retire %s, wanted to retire %d nodes (%s), but there are no spare nodes left.",
+ entry.getKey(), nodesThatShouldBeRetiredForFlavor.size(), commaSeparatedHostnames));
}
- return !parkedAll;
+ return ! nodesThatShouldBeRetiredForFlavor.isEmpty();
}).count();
return numFlavorsWithUnsuccessfullyRetiredNodes == 0;
@@ -88,7 +98,8 @@ public class NodeRetirer extends Maintainer {
void retireAllocated() {
List<Node> allNodes = nodeRepository().getNodes(NodeType.tenant);
List<ApplicationId> activeApplications = getActiveApplicationIds(allNodes);
- Map<Flavor, Long> numSpareNodesByFlavor = getNumberSpareReadyNodesByFlavor(allNodes);
+ Map<Flavor, Map<Node.State, Long>> numSpareNodesByFlavorByState = getNumberOfNodesByFlavorByNodeState(allNodes);
+ flavorSpareChecker.updateReadyAndActiveCountsByFlavor(numSpareNodesByFlavorByState);
for (ApplicationId applicationId : activeApplications) {
try (Mutex lock = nodeRepository().lock(applicationId)) {
@@ -99,20 +110,17 @@ public class NodeRetirer extends Maintainer {
for (Iterator<Node> iterator = retireableNodes.iterator(); iterator.hasNext() && numNodesAllowedToRetire > 0; ) {
Node retireableNode = iterator.next();
+ System.out.println(retireableNode);
- Set<Flavor> possibleReplacementFlavors = flavorClusters.getFlavorClusterFor(retireableNode.flavor());
- Flavor flavorWithMinSpareNodes = getMinAmongstKeys(numSpareNodesByFlavor, possibleReplacementFlavors);
- long spareNodesForMinFlavor = numSpareNodesByFlavor.getOrDefault(flavorWithMinSpareNodes, 0L);
- if (spareNodesForMinFlavor > 0) {
- log.info("Setting node " + retireableNode + " to wantToRetire and wantToDeprovision. Policy: " +
+ if (flavorSpareChecker.canRetireAllocatedNodeWithFlavor(retireableNode.flavor())) {
+ log.info("Setting wantToRetire for host " + retireableNode.hostname() +
+ " with flavor " + retireableNode.flavor().name() +
+ " allocated to " + retireableNode.allocation().get().owner() + ". Policy: " +
retirementPolicy.getClass().getSimpleName());
- Node updatedNode = retireableNode
- .with(retireableNode.status()
- .withWantToRetire(true)
- .withWantToDeprovision(true));
+ Node updatedNode = retireableNode.with(retireableNode.status()
+ .withWantToRetire(true)
+ .withWantToDeprovision(true));
nodeRepository().write(updatedNode);
- numSpareNodesByFlavor.put(flavorWithMinSpareNodes, spareNodesForMinFlavor - 1);
- numNodesAllowedToRetire--;
}
}
}
@@ -158,58 +166,10 @@ public class NodeRetirer extends Maintainer {
return Math.max(0, maxSimultaneousRetires - numNodesInWantToRetire);
}
- /**
- * Parks and sets wantToDeprovision for a subset of size 'limit' of nodes
- *
- * @param nodesToPark Nodes that we want to park
- * @param limit Maximum number of nodes we want to park
- * @return True iff we were able to park all the nodes
- */
- boolean limitedPark(Set<Node> nodesToPark, long limit) {
- nodesToPark.stream()
- .limit(limit)
- .forEach(node -> {
- nodeRepository().write(node.with(node.status().withWantToDeprovision(true)));
- nodeRepository().park(node.hostname(), Agent.NodeRetirer, "Policy: " + retirementPolicy.getClass().getSimpleName());
- });
-
- return limit >= nodesToPark.size();
- }
-
- Map<Flavor, Long> getNumberSpareReadyNodesByFlavor(List<Node> allNodes) {
- Map<Flavor, Long> numActiveNodesByFlavor = allNodes.stream()
- .filter(node -> node.state() == Node.State.active)
- .collect(Collectors.groupingBy(Node::flavor, Collectors.counting()));
-
+ private Map<Flavor, Map<Node.State, Long>> getNumberOfNodesByFlavorByNodeState(List<Node> allNodes) {
return allNodes.stream()
- .filter(node -> node.state() == Node.State.ready)
- .collect(Collectors.groupingBy(Node::flavor, Collectors.counting()))
- .entrySet().stream()
- .collect(Collectors.toMap(
- Map.Entry::getKey,
- entry -> {
- long numActiveNodesByCurrentFlavor = numActiveNodesByFlavor.getOrDefault(entry.getKey(), 0L);
- return getNumSpareNodes(numActiveNodesByCurrentFlavor, entry.getValue());
- }));
- }
-
- /**
- * Returns number of ready nodes to spare (beyond a safety buffer) for a flavor given its number of active
- * and ready nodes.
- */
- long getNumSpareNodes(long numActiveNodes, long numReadyNodes) {
- long numNodesToSpare = 2;
- return Math.max(0L, numReadyNodes - numNodesToSpare);
- }
-
- /**
- * Returns the key with the smallest value amongst keys
- */
- <K, V extends Comparable<V>> K getMinAmongstKeys(Map<K, V> map, Set<K> keys) {
- return map.entrySet().stream()
- .filter(entry -> keys.contains(entry.getKey()))
- .min(Comparator.comparing(Map.Entry::getValue))
- .map(Map.Entry::getKey)
- .orElseThrow(() -> new RuntimeException("No min key found"));
+ .collect(Collectors.groupingBy(
+ Node::flavor,
+ Collectors.groupingBy(Node::state, Collectors.counting())));
}
}
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClusters.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClusters.java
deleted file mode 100644
index d21c39581a4..00000000000
--- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClusters.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.provisioning;
-
-import com.yahoo.config.provision.Flavor;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Keeps track of flavor clusters: disjoint set of flavors that are connected through 'replaces'.
- * Given a node n, which has the flavor x, the flavor cluster of x is the set of flavors that
- * n could get next time it is redeployed.
- *
- * @author freva
- */
-public class FlavorClusters {
- final Set<Set<Flavor>> flavorClusters;
-
- public FlavorClusters(List<Flavor> flavors) {
- // Make each flavor and its immediate replacements own cluster
- Set<Set<Flavor>> prevClusters = flavors.stream()
- .map(flavor -> {
- Set<Flavor> cluster = new HashSet<>(flavor.replaces());
- cluster.add(flavor);
- return cluster;
- }).collect(Collectors.toSet());
-
- // See if any clusters intersect, if so merge them. Repeat until all the clusters are disjoint.
- while (true) {
- Set<Set<Flavor>> newClusters = new HashSet<>();
- for (Set<Flavor> oldCluster : prevClusters) {
- Optional<Set<Flavor>> overlappingCluster = newClusters.stream()
- .filter(cluster -> !Collections.disjoint(cluster, oldCluster))
- .findFirst();
-
- if (overlappingCluster.isPresent()) {
- overlappingCluster.get().addAll(oldCluster);
- } else {
- newClusters.add(oldCluster);
- }
- }
-
- if (prevClusters.size() == newClusters.size()) break;
- prevClusters = newClusters;
- }
-
- flavorClusters = prevClusters;
- }
-
- public Set<Flavor> getFlavorClusterFor(Flavor flavor) {
- return flavorClusters.stream()
- .filter(cluster -> cluster.contains(flavor))
- .findFirst()
- .orElseThrow(() -> new RuntimeException("Could not find cluster for flavor " + flavor));
- }
-}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTest.java
index 16f0acc9d55..5038b0f950b 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTest.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTest.java
@@ -1,360 +1,141 @@
package com.yahoo.vespa.hosted.provision.maintenance;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Flavor;
import com.yahoo.config.provision.NodeFlavors;
-import com.yahoo.config.provision.NodeType;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.maintenance.retire.RetirementPolicy;
import com.yahoo.vespa.hosted.provision.node.Agent;
-import com.yahoo.vespa.hosted.provision.node.Allocation;
-import com.yahoo.vespa.hosted.provision.node.History;
-import com.yahoo.vespa.hosted.provision.node.Status;
-import com.yahoo.vespa.hosted.provision.provisioning.FlavorClusters;
-import com.yahoo.vespa.hosted.provision.provisioning.FlavorClustersTest;
import org.junit.Before;
import org.junit.Test;
-import org.junit.experimental.runners.Enclosed;
-import org.junit.runner.RunWith;
-import java.time.Duration;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Optional;
import java.util.Set;
-import java.util.UUID;
import java.util.stream.Collectors;
-import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
/**
* @author freva
*/
-@RunWith(Enclosed.class)
public class NodeRetirerTest {
-
- public static class FullNodeRepositoryTester {
- private final RetirementPolicy policy = node -> node.ipAddresses().equals(Collections.singleton("::1"));
- private NodeRetirerTester tester;
- private NodeRetirer retirer;
-
- @Test
- public void testRetireUnallocatedNodes() {
- NodeFlavors nodeFlavors = FlavorClustersTest.makeFlavors(6);
- FlavorClusters flavorClusters = new FlavorClusters(nodeFlavors.getFlavors());
- tester = new NodeRetirerTester(nodeFlavors);
- retirer = new NodeRetirer(tester.nodeRepository, NodeRetirerTester.zone, flavorClusters, Duration.ofDays(1), new JobControl(tester.nodeRepository.database()), policy);
-
- tester.createReadyNodesByFlavor(7, 4, 77, 47);
- tester.deployApp("vespa", "calendar", 0, 3);
- tester.deployApp("vespa", "notes", 2, 12);
- tester.deployApp("sports", "results", 2, 7);
- tester.deployApp("search", "images", 3, 6);
-
- // Not all nodes that we wanted to retire could be retired now (Not enough spare nodes)
- assertSpareCountsByFlavor(2, 2, 56, 39);
- assertFalse(retirer.retireUnallocated());
- assertParkedCountsByFlavor(2, 2, 56, 39);
-
- assertSpareCountsByFlavor(0, 0, 0, 0);
- // Lets change parked nodes IP address and set it back to ready
- tester.nodeRepository.getNodes(Node.State.parked)
- .forEach(node -> {
- Agent parkingAgent = node.history().event(History.Event.Type.parked).orElseThrow(RuntimeException::new).agent();
- assertEquals(Agent.NodeRetirer, parkingAgent);
- assertTrue("Nodes parked by NodeRetirer should also have wantToDeprovision flag set", node.status().wantToDeprovision());
- tester.nodeRepository.write(node.withIpAddresses(Collections.singleton("::2")));
- tester.nodeRepository.setDirty(node.hostname());
- tester.nodeRepository.setReady(node.hostname());
- });
-
- // The remaining nodes we wanted to retire has been retired
- assertSpareCountsByFlavor(2, 2, 56, 39);
- assertTrue(retirer.retireUnallocated());
- assertParkedCountsByFlavor(2, 2, 2, 2);
- }
-
- /* Creates flavors where 'replaces' graph and node counts that looks like this:
- * Total nodes: 40 1
- * | 4 Total nodes: 8
- * Total nodes: 20 | | search.images nodes: 4
- * vespa.notes nodes: 3 0 |
- * sports.results nodes: 6 / \ 5 Total nodes: 6
- * / \ Total nodes: 14 search.videos nodes: 2
- * Total nodes: 25 2 3 vespa.calendar nodes: 7
- */
- @Test
- public void testRetireAllocatedNodes() throws InterruptedException {
- NodeFlavors nodeFlavors = FlavorClustersTest.makeFlavors(
- Collections.singletonList(1), // 0 -> {1}
- Collections.emptyList(), // 1 -> {}
- Collections.singletonList(0), // 2 -> {0}
- Collections.singletonList(0), // 3 -> {0}
- Collections.emptyList(), // 4 -> {}
- Collections.singletonList(4)); // 5 -> {4}
- FlavorClusters flavorClusters = new FlavorClusters(nodeFlavors.getFlavors());
- tester = new NodeRetirerTester(nodeFlavors);
-
- tester.createReadyNodesByFlavor(21, 42, 27, 15, 8, 6);
- tester.deployApp("vespa", "calendar", 3, 7);
- tester.deployApp("vespa", "notes", 0, 3);
- tester.deployApp("sports", "results", 0, 6);
- tester.deployApp("search", "images", 4, 4);
- tester.deployApp("search", "videos", 5, 2);
-
- JobControl jobControl = new JobControl(tester.nodeRepository.database());
- retirer = new NodeRetirer(tester.nodeRepository, NodeRetirerTester.zone, flavorClusters, Duration.ofDays(1), jobControl, policy);
- // Update IP addresses on ready nodes so that when they are deployed to, we wont retire them
- tester.nodeRepository.getNodes(Node.State.ready)
- .forEach(node -> tester.nodeRepository.write(node.withIpAddresses(Collections.singleton("::2"))));
-
- assertSpareCountsByFlavor(10, 40, 25, 6, 2, 2);
-
-
- retireThenAssertSpareAndParkedCounts(new long[]{8, 40, 25, 5, 1, 1}, new long[]{1, 1, 1, 1, 1});
-
- // At this point we only have 1 spare node for flavors 4 & 5, 5 also replaces 4, which means that we can
- // only replace 1 of either flavor-4 or flavor-5.
- // search.videos (5th app) wont be replaced because search.images will get the last spare node in
- // flavor-4, flavor-5 cluster because it has more active nodes
- retireThenAssertSpareAndParkedCounts(new long[]{6, 40, 25, 4, 0, 1}, new long[]{2, 2, 2, 2, 1});
-
- // After redeploying search.images, it ended up on a flavor-4 node, so we still have a flavor-5 spare,
- // but we still wont be able to retire any nodes for search.videos as min spare for its flavor cluster is 0
- retireThenAssertSpareAndParkedCounts(new long[]{4, 40, 25, 3, 0, 1}, new long[]{3, 3, 3, 2, 1});
-
- // All 3 of vespa.notes old nodes have been retired, so its parked count should stay the same
- retireThenAssertSpareAndParkedCounts(new long[]{3, 40, 25, 2, 0, 1}, new long[]{4, 3, 4, 2, 1});
-
- // Only vespa.calendar and sports.results remain, but their flavors (3 and 0 respectively) are in the same
- // flavor cluster, because the min count for this cluster is 1, we can only retire one of them
- retireThenAssertSpareAndParkedCounts(new long[]{2, 40, 25, 1, 0, 1}, new long[]{5, 3, 5, 2, 1});
-
- // min flavor count for both flavor clusters is now 0, so no further change is expected
- retireThenAssertSpareAndParkedCounts(new long[]{2, 40, 25, 0, 0, 1}, new long[]{6, 3, 5, 2, 1});
- retireThenAssertSpareAndParkedCounts(new long[]{2, 40, 25, 0, 0, 1}, new long[]{6, 3, 5, 2, 1});
-
- tester.nodeRepository.getNodes(Node.State.parked)
- .forEach(node -> assertTrue("Nodes parked by NodeRetirer should also have wantToDeprovision flag set",
- node.status().wantToDeprovision()));
- }
-
- @Test
- public void testGetActiveApplicationIds() {
- NodeFlavors nodeFlavors = FlavorClustersTest.makeFlavors(1);
- FlavorClusters flavorClusters = new FlavorClusters(nodeFlavors.getFlavors());
- tester = new NodeRetirerTester(nodeFlavors);
- retirer = new NodeRetirer(tester.nodeRepository, NodeRetirerTester.zone, flavorClusters, Duration.ofDays(1), new JobControl(tester.nodeRepository.database()), policy);
-
- tester.createReadyNodesByFlavor(50);
- ApplicationId a1 = tester.deployApp("vespa", "calendar", 0, 10);
- ApplicationId a2 = tester.deployApp("vespa", "notes", 0, 12);
- ApplicationId a3 = tester.deployApp("sports", "results", 0, 7);
- ApplicationId a4 = tester.deployApp("search", "images", 0, 6);
-
- List<ApplicationId> expectedOrder = Arrays.asList(a2, a1, a3, a4);
- List<ApplicationId> actualOrder = retirer.getActiveApplicationIds(tester.nodeRepository.getNodes());
- assertEquals(expectedOrder, actualOrder);
- }
-
- @Test
- public void testGetRetireableNodesForApplication() {
- NodeFlavors nodeFlavors = FlavorClustersTest.makeFlavors(1);
- FlavorClusters flavorClusters = new FlavorClusters(nodeFlavors.getFlavors());
- tester = new NodeRetirerTester(nodeFlavors);
- retirer = new NodeRetirer(tester.nodeRepository, NodeRetirerTester.zone, flavorClusters, Duration.ofDays(1), new JobControl(tester.nodeRepository.database()), policy);
-
- tester.createReadyNodesByFlavor(10);
- tester.deployApp("vespa", "calendar", 0, 10);
-
- List<Node> nodes = tester.nodeRepository.getNodes();
- Set<String> actual = retirer.getRetireableNodesForApplication(nodes).stream().map(Node::hostname).collect(Collectors.toSet());
- Set<String> expected = nodes.stream().map(Node::hostname).collect(Collectors.toSet());
- assertEquals(expected, actual);
-
- Node nodeWantToRetire = tester.nodeRepository.getNode("host3.test.yahoo.com").orElseThrow(RuntimeException::new);
- tester.nodeRepository.write(nodeWantToRetire.with(nodeWantToRetire.status().withWantToRetire(true)));
- Node nodeToFail = tester.nodeRepository.getNode("host5.test.yahoo.com").orElseThrow(RuntimeException::new);
- tester.nodeRepository.fail(nodeToFail.hostname(), Agent.system, "Failed for unit testing");
- Node nodeToUpdate = tester.nodeRepository.getNode("host8.test.yahoo.com").orElseThrow(RuntimeException::new);
- tester.nodeRepository.write(nodeToUpdate.withIpAddresses(Collections.singleton("::2")));
-
- nodes = tester.nodeRepository.getNodes();
- Set<String> excluded = Stream.of(nodeWantToRetire, nodeToFail, nodeToUpdate).map(Node::hostname).collect(Collectors.toSet());
- Set<String> actualAfterUpdates = retirer.getRetireableNodesForApplication(nodes).stream().map(Node::hostname).collect(Collectors.toSet());
- Set<String> expectedAfterUpdates = nodes.stream().map(Node::hostname).filter(node -> !excluded.contains(node)).collect(Collectors.toSet());
- assertEquals(expectedAfterUpdates, actualAfterUpdates);
- }
-
- @Test
- public void testGetNumberNodesAllowToRetireForApplication() {
- NodeFlavors nodeFlavors = FlavorClustersTest.makeFlavors(1);
- FlavorClusters flavorClusters = new FlavorClusters(nodeFlavors.getFlavors());
- tester = new NodeRetirerTester(nodeFlavors);
- retirer = new NodeRetirer(tester.nodeRepository, NodeRetirerTester.zone, flavorClusters, Duration.ofDays(1), new JobControl(tester.nodeRepository.database()), policy);
-
- tester.createReadyNodesByFlavor(10);
- tester.deployApp("vespa", "calendar", 0, 10);
-
- long actualAllActive = retirer.getNumberNodesAllowToRetireForApplication(tester.nodeRepository.getNodes(), 2);
- assertEquals(2, actualAllActive);
-
- // Lets put 3 random nodes in wantToRetire
- List<Node> nodesToRetire = tester.nodeRepository.getNodes().stream().limit(3).collect(Collectors.toList());
- nodesToRetire.forEach(node -> tester.nodeRepository.write(node.with(node.status().withWantToRetire(true))));
- long actualOneWantToRetire = retirer.getNumberNodesAllowToRetireForApplication(tester.nodeRepository.getNodes(), 2);
- assertEquals(0, actualOneWantToRetire);
-
- // Now 2 of those finish retiring and go to parked
- nodesToRetire.stream().limit(2).forEach(node ->
- tester.nodeRepository.park(node.hostname(), Agent.system, "Parked for unit testing"));
- long actualOneRetired = retirer.getNumberNodesAllowToRetireForApplication(tester.nodeRepository.getNodes(), 2);
- assertEquals(1, actualOneRetired);
- }
-
- private void assertSpareCountsByFlavor(long... nums) {
- Map<Flavor, Long> expectedSpareCountsByFlavor = tester.expectedCountsByFlavor(nums);
- Map<Flavor, Long> actualSpaceCountsByFlavor = retirer.getNumberSpareReadyNodesByFlavor(tester.nodeRepository.getNodes());
- assertEquals(expectedSpareCountsByFlavor, actualSpaceCountsByFlavor);
- }
-
- private void assertParkedCountsByFlavor(long... nums) {
- Map<Flavor, Long> expected = tester.expectedCountsByFlavor(nums);
- Map<Flavor, Long> actual = tester.nodeRepository.getNodes(Node.State.parked).stream()
- .collect(Collectors.groupingBy(Node::flavor, Collectors.counting()));
- assertEquals(expected, actual);
- }
-
- private void assertParkedCountsByApplication(long... nums) {
- Map<ApplicationId, Long> expected = tester.expectedCountsByApplication(nums);
- Map<ApplicationId, Long> actual = tester.nodeRepository.getNodes(Node.State.parked).stream()
- .collect(Collectors.groupingBy(node -> node.allocation().get().owner(), Collectors.counting()));
- assertEquals(expected, actual);
- }
-
- private void retireThenAssertSpareAndParkedCounts(long[] spareCountsByFlavor, long[] parkedCountsByApp) {
- retirer.retireAllocated();
- tester.iterateMaintainers();
- assertSpareCountsByFlavor(spareCountsByFlavor);
- assertParkedCountsByApplication(parkedCountsByApp);
- }
+ private final RetirementPolicy policy = node -> node.ipAddresses().equals(Collections.singleton("::1"));
+ private NodeRetirerTester tester;
+ private NodeRetirer retirer;
+
+ @Before
+ public void setup() {
+ NodeFlavors nodeFlavors = NodeRetirerTester.makeFlavors(5);
+ tester = new NodeRetirerTester(nodeFlavors);
+ retirer = tester.makeNodeRetirer(policy);
+
+ tester.createReadyNodesByFlavor(21, 42, 27, 15, 8);
+ tester.deployApp("vespa", "calendar", 3, 7);
+ tester.deployApp("vespa", "notes", 0, 3);
+ tester.deployApp("sports", "results", 0, 6);
+ tester.deployApp("search", "images", 3, 4);
+ tester.deployApp("search", "videos", 2, 2);
}
- /**
- * For testing methods that require minimal node repository and flavor setup
- */
- public static class HelperMethodsTester {
- private final List<Flavor> flavors = FlavorClustersTest.makeFlavors(5).getFlavors();
- private final List<Node> nodes = new ArrayList<>();
- private final NodeRetirer retirer = mock(NodeRetirer.class);
-
- @Test
- public void testGetNumberSpareNodesWithNoActiveNodes() {
- addNodesByFlavor(Node.State.ready, 5, 3, 77);
+ @Test
+ public void testRetireUnallocated() {
+ tester.assertCountsForStateByFlavor(Node.State.ready, 12, 42, 25, 4, 8);
+ tester.setNumberAllowedUnallocatedRetirementsPerFlavor(6, 30, 20, 2, 4);
+ assertFalse(retirer.retireUnallocated());
+ tester.assertCountsForStateByFlavor(Node.State.parked, 6, 30, 20, 2, 4);
- Map<Flavor, Long> expected = expectedCountsByFlavor(3, 1, 75);
- Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(nodes);
- assertEquals(expected, actual);
- }
+ tester.assertCountsForStateByFlavor(Node.State.ready, 6, 12, 5, 2, 4);
+ tester.setNumberAllowedUnallocatedRetirementsPerFlavor(10, 20, 5, 5, 4);
+ assertTrue(retirer.retireUnallocated());
+ tester.assertCountsForStateByFlavor(Node.State.parked, 12, 42, 25, 4, 8);
- @Test
- public void testGetNumberSpareNodesWithActiveNodes() {
- addNodesByFlavor(Node.State.ready, 5, 3, 77, 47);
- addNodesByFlavor(Node.State.active, 0, 10, 2, 230, 137);
-
- Map<Flavor, Long> expected = expectedCountsByFlavor(3, 1, 75, 45);
- Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(nodes);
- assertEquals(expected, actual);
- }
-
- @Before
- public void setup() {
- when(retirer.getNumSpareNodes(any(Long.class), any(Long.class))).thenCallRealMethod();
- when(retirer.getNumberSpareReadyNodesByFlavor(any())).thenCallRealMethod();
- }
-
- private Map<Flavor, Long> expectedCountsByFlavor(int... nums) {
- Map<Flavor, Long> countsByFlavor = new HashMap<>();
- for (int i = 0; i < nums.length; i++) {
- Flavor flavor = flavors.get(i);
- countsByFlavor.put(flavor, (long) nums[i]);
- }
- return countsByFlavor;
- }
-
- private void addNodesByFlavor(Node.State state, int... nums) {
- for (int i = 0; i < nums.length; i++) {
- Flavor flavor = flavors.get(i);
- for (int j = 0; j < nums[i]; j++) {
- int id = nodes.size();
- Node node = createNode("host-" + id + ".yahoo.com", flavor, state, Optional.empty(), Collections.singleton("::1"));
- nodes.add(node);
- }
- }
- }
-
- private Node createNode(String hostname, Flavor flavor, Node.State state, Optional<Allocation> allocation, Set<String> ipAddresses) {
- return new Node(
- UUID.randomUUID().toString(),
- ipAddresses,
- Collections.emptySet(),
- hostname,
- Optional.empty(),
- flavor,
- Status.initial(),
- state,
- allocation,
- History.empty(),
- NodeType.tenant);
- }
+ tester.nodeRepository.getNodes().forEach(node ->
+ assertEquals(node.status().wantToDeprovision(), node.state() == Node.State.parked));
}
- /**
- * For testing methods that require no internal state and independent of other methods
- */
- public static class IndependentMethodTester {
- private final NodeRetirer retirer = mock(NodeRetirer.class);
-
- @Test
- public void testGetNumSpareNodes() {
- when(retirer.getNumSpareNodes(any(Long.class), any(Long.class))).thenCallRealMethod();
-
- assertEquals(retirer.getNumSpareNodes(0, 0), 0L);
- assertEquals(retirer.getNumSpareNodes(0, 1), 0L);
- assertEquals(retirer.getNumSpareNodes(0, 100), 98L);
-
- assertEquals(retirer.getNumSpareNodes(1, 0), 0L);
- assertEquals(retirer.getNumSpareNodes(1, 1), 0L);
- assertEquals(retirer.getNumSpareNodes(1, 2), 0L);
- assertEquals(retirer.getNumSpareNodes(43, 23), 21L);
- }
-
- @Test
- public void testGetMinAmongstKeys() {
- when(retirer.getMinAmongstKeys(any(), any())).thenCallRealMethod();
+ @Test
+ public void testRetireAllocated() {
+ // Update IP addresses on ready nodes so that when they are deployed to, we wont retire them
+ tester.nodeRepository.getNodes(Node.State.ready)
+ .forEach(node -> tester.nodeRepository.write(node.withIpAddresses(Collections.singleton("::2"))));
+
+ tester.assertCountsForStateByFlavor(Node.State.active, 9, -1, 2, 11, -1);
+ System.out.println("start");
+ tester.setNumberAllowedAllocatedRetirementsPerFlavor(3, 2, 3, 2);
+ retirer.retireAllocated();
+ tester.assertParkedCountsByApplication(-1, -1, -1, -1, -1); // Nodes should be in retired, but not yet parked
+
+ tester.iterateMaintainers();
+ tester.assertParkedCountsByApplication(1, 1, 1, 1, 1);
+
+ // We can only retire 1 more of flavor 0 and 1 more of flavor 2, app 3 is the largest that is on flavor 0
+ // and app 5 is the only one on flavor 2
+ retirer.retireAllocated();
+ tester.iterateMaintainers();
+ tester.assertParkedCountsByApplication(1, 1, 2, 1, 2);
+
+ // No more retirements are possible
+ retirer.retireAllocated();
+ tester.iterateMaintainers();
+ tester.assertParkedCountsByApplication(1, 1, 2, 1, 2);
+
+ tester.nodeRepository.getNodes().forEach(node ->
+ assertEquals(node.status().wantToDeprovision(), node.state() == Node.State.parked));
+ }
- Map<String, Integer> map = createMapWith(4, 10, 43, 23, 7, 53, 2, 12, 42, 10);
- Set<String> keys = createKeySetWith(1, 3, 4, 5, 7, 9);
- assertEquals("4", retirer.getMinAmongstKeys(map, keys)); // Smallest value is 7, which is index 4
- }
+ @Test
+ public void testGetActiveApplicationIds() {
+ List<String> expectedOrder = Arrays.asList(
+ "vespa.calendar", "sports.results", "search.images", "vespa.notes", "search.videos");
+ List<String> actualOrder = retirer.getActiveApplicationIds(tester.nodeRepository.getNodes()).stream()
+ .map(applicationId -> applicationId.toShortString().replace(":default", ""))
+ .collect(Collectors.toList());
+ assertEquals(expectedOrder, actualOrder);
+ }
- private Map<String, Integer> createMapWith(int... values) {
- return IntStream.range(0, values.length).boxed().collect(Collectors.toMap(String::valueOf, i -> values[i]));
- }
+ @Test
+ public void testGetRetireableNodesForApplication() {
+ ApplicationId app = new ApplicationId.Builder().tenant("vespa").applicationName("calendar").build();
+
+ List<Node> nodes = tester.nodeRepository.getNodes(app);
+ Set<String> actual = retirer.getRetireableNodesForApplication(nodes).stream().map(Node::hostname).collect(Collectors.toSet());
+ Set<String> expected = nodes.stream().map(Node::hostname).collect(Collectors.toSet());
+ assertEquals(expected, actual);
+
+ Node nodeWantToRetire = tester.nodeRepository.getNode("host3.test.yahoo.com").orElseThrow(RuntimeException::new);
+ tester.nodeRepository.write(nodeWantToRetire.with(nodeWantToRetire.status().withWantToRetire(true)));
+ Node nodeToFail = tester.nodeRepository.getNode("host5.test.yahoo.com").orElseThrow(RuntimeException::new);
+ tester.nodeRepository.fail(nodeToFail.hostname(), Agent.system, "Failed for unit testing");
+ Node nodeToUpdate = tester.nodeRepository.getNode("host8.test.yahoo.com").orElseThrow(RuntimeException::new);
+ tester.nodeRepository.write(nodeToUpdate.withIpAddresses(Collections.singleton("::2")));
+
+ nodes = tester.nodeRepository.getNodes(app);
+ Set<String> excluded = Stream.of(nodeWantToRetire, nodeToFail, nodeToUpdate).map(Node::hostname).collect(Collectors.toSet());
+ Set<String> actualAfterUpdates = retirer.getRetireableNodesForApplication(nodes).stream().map(Node::hostname).collect(Collectors.toSet());
+ Set<String> expectedAfterUpdates = nodes.stream().map(Node::hostname).filter(node -> !excluded.contains(node)).collect(Collectors.toSet());
+ assertEquals(expectedAfterUpdates, actualAfterUpdates);
+ }
- private Set<String> createKeySetWith(int... keys) {
- return Arrays.stream(keys).boxed().map(String::valueOf).collect(Collectors.toSet());
- }
+ @Test
+ public void testGetNumberNodesAllowToRetireForApplication() {
+ ApplicationId app = new ApplicationId.Builder().tenant("vespa").applicationName("calendar").build();
+ long actualAllActive = retirer.getNumberNodesAllowToRetireForApplication(tester.nodeRepository.getNodes(app), 2);
+ assertEquals(2, actualAllActive);
+
+ // Lets put 3 random nodes in wantToRetire
+ List<Node> nodesToRetire = tester.nodeRepository.getNodes(app).stream().limit(3).collect(Collectors.toList());
+ nodesToRetire.forEach(node -> tester.nodeRepository.write(node.with(node.status().withWantToRetire(true))));
+ long actualOneWantToRetire = retirer.getNumberNodesAllowToRetireForApplication(tester.nodeRepository.getNodes(app), 2);
+ assertEquals(0, actualOneWantToRetire);
+
+ // Now 2 of those finish retiring and go to parked
+ nodesToRetire.stream().limit(2).forEach(node ->
+ tester.nodeRepository.park(node.hostname(), Agent.system, "Parked for unit testing"));
+ long actualOneRetired = retirer.getNumberNodesAllowToRetireForApplication(tester.nodeRepository.getNodes(app), 2);
+ assertEquals(1, actualOneRetired);
}
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTester.java
index c6412d7c28f..7622cbb1714 100644
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTester.java
+++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTester.java
@@ -19,12 +19,16 @@ import com.yahoo.vespa.curator.mock.MockCurator;
import com.yahoo.vespa.curator.transaction.CuratorTransaction;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.maintenance.retire.RetirementPolicy;
+import com.yahoo.vespa.hosted.provision.provisioning.FlavorConfigBuilder;
+import com.yahoo.vespa.hosted.provision.provisioning.FlavorSpareChecker;
import com.yahoo.vespa.hosted.provision.provisioning.NodeRepositoryProvisioner;
import com.yahoo.vespa.hosted.provision.testutils.MockDeployer;
import com.yahoo.vespa.hosted.provision.testutils.MockNameResolver;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -35,6 +39,10 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
* @author freva
@@ -43,29 +51,35 @@ public class NodeRetirerTester {
public static final Zone zone = new Zone(Environment.prod, RegionName.from("us-east"));
// Components with state
- public final ManualClock clock;
+ public final ManualClock clock = new ManualClock();
public final NodeRepository nodeRepository;
- private final NodeRepositoryProvisioner provisioner;
- private final Curator curator;
+ private final FlavorSpareChecker flavorSpareChecker = mock(FlavorSpareChecker.class);
+ private final Curator curator = new MockCurator();
+ private final MockDeployer deployer;
+ private final JobControl jobControl;
private final List<Flavor> flavors;
+ private final NodeRepositoryProvisioner provisioner;
// Use LinkedHashMap to keep order in which applications were deployed
private final Map<ApplicationId, MockDeployer.ApplicationContext> apps = new LinkedHashMap<>();
- private PeriodicApplicationMaintainer applicationMaintainer;
private RetiredExpirer retiredExpirer;
private InactiveExpirer inactiveExpirer;
private int nextNodeId = 0;
- public NodeRetirerTester(NodeFlavors nodeFlavors) {
- clock = new ManualClock();
- curator = new MockCurator();
+ NodeRetirerTester(NodeFlavors nodeFlavors) {
nodeRepository = new NodeRepository(nodeFlavors, curator, clock, zone, new MockNameResolver().mockAnyLookup());
+ jobControl = new JobControl(nodeRepository.database());
provisioner = new NodeRepositoryProvisioner(nodeRepository, nodeFlavors, zone);
+ deployer = new MockDeployer(provisioner, apps);
flavors = nodeFlavors.getFlavors().stream().sorted(Comparator.comparing(Flavor::name)).collect(Collectors.toList());
}
- public void createReadyNodesByFlavor(int... nums) {
+ NodeRetirer makeNodeRetirer(RetirementPolicy policy) {
+ return new NodeRetirer(nodeRepository, zone, flavorSpareChecker, Duration.ofDays(1), deployer, jobControl, policy);
+ }
+
+ void createReadyNodesByFlavor(int... nums) {
List<Node> nodes = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
Flavor flavor = flavors.get(i);
@@ -81,7 +95,7 @@ public class NodeRetirerTester {
nodeRepository.setReady(nodes);
}
- public ApplicationId deployApp(String tenantName, String applicationName, int flavorId, int numNodes) {
+ void deployApp(String tenantName, String applicationName, int flavorId, int numNodes) {
Flavor flavor = flavors.get(flavorId);
ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default");
@@ -90,22 +104,14 @@ public class NodeRetirerTester {
apps.put(applicationId, new MockDeployer.ApplicationContext(applicationId, cluster, capacity, 1));
activate(applicationId, cluster, capacity);
- return applicationId;
}
- public void iterateMaintainers() {
- if (applicationMaintainer == null) {
- MockDeployer deployer = new MockDeployer(provisioner, apps);
- JobControl jobControl = new JobControl(nodeRepository.database());
- applicationMaintainer = new PeriodicApplicationMaintainerTest.TestablePeriodicApplicationMaintainer(
- deployer, nodeRepository, Duration.ofMinutes(10), Optional.empty());
+ void iterateMaintainers() {
+ if (retiredExpirer == null) {
retiredExpirer = new RetiredExpirer(nodeRepository, deployer, clock, Duration.ofMinutes(10), jobControl);
inactiveExpirer = new InactiveExpirer(nodeRepository, clock, Duration.ofMinutes(10), jobControl);
-
}
- applicationMaintainer.maintain();
-
clock.advance(Duration.ofMinutes(11));
retiredExpirer.maintain();
@@ -120,7 +126,39 @@ public class NodeRetirerTester {
transaction.commit();
}
- public Map<Flavor, Long> expectedCountsByFlavor(long... nums) {
+ void setNumberAllowedUnallocatedRetirementsPerFlavor(int... numAllowed) {
+ for (int i = 0; i < numAllowed.length; i++) {
+ Boolean[] responses = new Boolean[numAllowed[i]];
+ Arrays.fill(responses, true);
+ responses[responses.length - 1 ] = false;
+ when(flavorSpareChecker.canRetireUnallocatedNodeWithFlavor(eq(flavors.get(i)))).thenReturn(true, responses);
+ }
+ }
+
+ void setNumberAllowedAllocatedRetirementsPerFlavor(int... numAllowed) {
+ for (int i = 0; i < numAllowed.length; i++) {
+ Boolean[] responses = new Boolean[numAllowed[i]];
+ Arrays.fill(responses, true);
+ responses[responses.length - 1] = false;
+ when(flavorSpareChecker.canRetireAllocatedNodeWithFlavor(eq(flavors.get(i)))).thenReturn(true, responses);
+ }
+ }
+
+ void assertCountsForStateByFlavor(Node.State state, long... nums) {
+ Map<Flavor, Long> expected = expectedCountsByFlavor(nums);
+ Map<Flavor, Long> actual = nodeRepository.getNodes(state).stream()
+ .collect(Collectors.groupingBy(Node::flavor, Collectors.counting()));
+ assertEquals(expected, actual);
+ }
+
+ void assertParkedCountsByApplication(long... nums) {
+ Map<ApplicationId, Long> expected = expectedCountsByApplication(nums);
+ Map<ApplicationId, Long> actual = nodeRepository.getNodes(Node.State.parked).stream()
+ .collect(Collectors.groupingBy(node -> node.allocation().get().owner(), Collectors.counting()));
+ assertEquals(expected, actual);
+ }
+
+ private Map<Flavor, Long> expectedCountsByFlavor(long... nums) {
Map<Flavor, Long> countsByFlavor = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (nums[i] < 0) continue;
@@ -130,14 +168,22 @@ public class NodeRetirerTester {
return countsByFlavor;
}
- public Map<ApplicationId, Long> expectedCountsByApplication(long... nums) {
+ private Map<ApplicationId, Long> expectedCountsByApplication(long... nums) {
Map<ApplicationId, Long> countsByApplicationId = new HashMap<>();
Iterator<ApplicationId> iterator = apps.keySet().iterator();
for (int i = 0; iterator.hasNext(); i++) {
- if (nums[i] < 0) continue;
ApplicationId applicationId = iterator.next();
+ if (nums[i] < 0) continue;
countsByApplicationId.put(applicationId, nums[i]);
}
return countsByApplicationId;
}
+
+ static NodeFlavors makeFlavors(int numFlavors) {
+ FlavorConfigBuilder flavorConfigBuilder = new FlavorConfigBuilder();
+ for (int i = 0; i < numFlavors; i++) {
+ flavorConfigBuilder.addFlavor("flavor-" + i, 1. /* cpu*/, 3. /* mem GB*/, 2. /*disk GB*/, Flavor.Type.BARE_METAL);
+ }
+ return new NodeFlavors(flavorConfigBuilder.build());
+ }
}
diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClustersTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClustersTest.java
deleted file mode 100644
index b195c763a90..00000000000
--- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/FlavorClustersTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.provision.provisioning;
-
-import com.yahoo.config.provision.Flavor;
-import com.yahoo.config.provision.NodeFlavors;
-import com.yahoo.config.provisioning.FlavorsConfig;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.junit.Assert.assertEquals;
-
-/**
- * @author freva
- */
-@SuppressWarnings("unchecked")
-public class FlavorClustersTest {
-
- @Test
- public void testSingletonClusters() {
- NodeFlavors nodeFlavors = makeFlavors(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
- FlavorClusters clusters = new FlavorClusters(nodeFlavors.getFlavors());
- Set<Set<Flavor>> expectedClusters = createExpectedClusters(nodeFlavors,
- Collections.singletonList(0), Collections.singletonList(1), Collections.singletonList(2));
- assertEquals(expectedClusters, clusters.flavorClusters);
- }
-
- @Test
- public void testSingleClusterWithMultipleNodes() {
- // 0 -> 1 -> 2
- NodeFlavors nodeFlavors = makeFlavors(Collections.singletonList(1), Collections.singletonList(2), Collections.emptyList());
- FlavorClusters clusters = new FlavorClusters(nodeFlavors.getFlavors());
- Set<Set<Flavor>> expectedClusters = createExpectedClusters(nodeFlavors, Arrays.asList(0, 1, 2));
- assertEquals(expectedClusters, clusters.flavorClusters);
- }
-
- @Test
- public void testMultipleClustersWithMultipleNodes() {
- /* Creates flavors where 'replaces' graph that looks like this:
- * 5
- * |
- * |
- * 3 4 8
- * \ / |
- * \ / |
- * 1 6 7
- * / \
- * / \
- * 0 2
- */
- NodeFlavors nodeFlavors = makeFlavors(
- Collections.singletonList(1), // 0 -> {1}
- Arrays.asList(3, 4), // 1 -> {3, 4}
- Collections.singletonList(1), // 2 -> {1}
- Collections.singletonList(5), // 3 -> {5}
- Collections.emptyList(), // 4 -> {}
- Collections.emptyList(), // 5 -> {}
- Collections.emptyList(), // 6 -> {}
- Collections.singletonList(8), // 7 -> {8}
- Collections.emptyList()); // 8 -> {}
-
- FlavorClusters clusters = new FlavorClusters(nodeFlavors.getFlavors());
- Set<Set<Flavor>> expectedClusters = createExpectedClusters(nodeFlavors,
- Arrays.asList(0, 1, 2, 3, 4, 5),
- Collections.singletonList(6),
- Arrays.asList(7, 8));
- assertEquals(expectedClusters, clusters.flavorClusters);
- }
-
- private Set<Set<Flavor>> createExpectedClusters(NodeFlavors nodeFlavors, List<Integer>... clusters) {
- return Arrays.stream(clusters).map(cluster ->
- cluster.stream()
- .map(flavorId -> nodeFlavors.getFlavorOrThrow("flavor-" + flavorId))
- .collect(Collectors.toSet()))
- .collect(Collectors.toSet());
- }
-
- public static NodeFlavors makeFlavors(int numFlavors) {
- FlavorConfigBuilder flavorConfigBuilder = new FlavorConfigBuilder();
- for (int i = 0; i < numFlavors; i++) {
- flavorConfigBuilder.addFlavor("flavor-" + i, 1. /* cpu*/, 3. /* mem GB*/, 2. /*disk GB*/, Flavor.Type.BARE_METAL);
- }
- return new NodeFlavors(flavorConfigBuilder.build());
- }
-
- /**
- * Takes in variable number of List of Integers:
- * For each list a flavor is created
- * For each element, n, in list, the new flavor replace n'th flavor
- */
- @SafeVarargs
- public static NodeFlavors makeFlavors(List<Integer>... replaces) {
- FlavorConfigBuilder flavorConfigBuilder = new FlavorConfigBuilder();
- for (int i = 0; i < replaces.length; i++) {
- FlavorsConfig.Flavor.Builder builder = flavorConfigBuilder
- .addFlavor("flavor-" + i, 1. /* cpu*/, 3. /* mem GB*/, 2. /*disk GB*/, Flavor.Type.BARE_METAL);
-
- for (Integer replacesId : replaces[i]) {
- flavorConfigBuilder.addReplaces("flavor-" + replacesId, builder);
- }
- }
- return new NodeFlavors(flavorConfigBuilder.build());
- }
-}