diff options
author | valerijf <valerijf@yahoo-inc.com> | 2017-06-08 15:35:04 +0200 |
---|---|---|
committer | valerijf <valerijf@yahoo-inc.com> | 2017-06-08 15:35:04 +0200 |
commit | 4c05f765f772674fc248e8337558616da3cf7326 (patch) | |
tree | 057a8b5e86bcdce23241629727778c5358aa4cc9 /node-repository | |
parent | f1665f30a371776124225a50a80907fdb24e0469 (diff) |
Update NodeRetirer to use FlavorSpareChecker
Diffstat (limited to 'node-repository')
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()); - } -} |