diff options
author | valerijf <valerijf@yahoo-inc.com> | 2017-04-27 13:01:13 +0200 |
---|---|---|
committer | valerijf <valerijf@yahoo-inc.com> | 2017-04-27 13:03:26 +0200 |
commit | cca34767a2462f73f36ab9abbb36dd0ba6b304c3 (patch) | |
tree | 595b490777d1231dd0551b5727b51e2757f015f8 /node-repository | |
parent | 3b111667ee4ed3389bbe5bd9a2d3af19175e6a06 (diff) |
Rewritten NodeRetirer test to work on non-mocked node-repository
Diffstat (limited to 'node-repository')
3 files changed, 255 insertions, 164 deletions
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 4c0c670a2ff..18be5f7661e 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 @@ -21,10 +21,12 @@ import java.util.stream.Collectors; public class NodeRetirer extends Maintainer { private final RetirementPolicy retirementPolicy; - public NodeRetirer(NodeRepository nodeRepository, Zone zone, Duration interval, RetirementPolicy retirementPolicy, Zone... applies) { - super(nodeRepository, interval); - this.retirementPolicy = retirementPolicy; + public NodeRetirer(NodeRepository nodeRepository, Zone zone, Duration interval, JobControl jobControl, + RetirementPolicy retirementPolicy, Zone... applies) { + super(nodeRepository, interval, jobControl); if (! Arrays.asList(applies).contains(zone)) deconstruct(); + + this.retirementPolicy = retirementPolicy; } @Override @@ -62,7 +64,7 @@ public class NodeRetirer extends Maintainer { boolean limitedPark(Set<Node> nodesToPark, long limit) { nodesToPark.stream() .limit(limit) - .forEach(node -> nodeRepository().park(node.hostname(), Agent.NodeRetirer, "Parked by NodeRetirer, Policy: " + retirementPolicy.getClass().getName())); + .forEach(node -> nodeRepository().park(node.hostname(), Agent.NodeRetirer, "Parked by NodeRetirer, Policy: " + retirementPolicy.getClass().getSimpleName())); return limit >= nodesToPark.size(); } @@ -92,9 +94,4 @@ public class NodeRetirer extends Maintainer { long numNodesToSpare = (long) Math.ceil(0.1 * numActiveNodes); return Math.max(0L, numReadyNodes - numNodesToSpare); } - - @Override - public String toString() { - return "Node retirer"; - } } 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 929b3d8da1f..628d334c2c0 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,25 +1,16 @@ package com.yahoo.vespa.hosted.provision.maintenance; -import com.yahoo.component.Version; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.ClusterMembership; -import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.Flavor; import com.yahoo.config.provision.NodeType; -import com.yahoo.config.provision.RegionName; -import com.yahoo.config.provision.Zone; 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.node.Allocation; -import com.yahoo.vespa.hosted.provision.node.Generation; import com.yahoo.vespa.hosted.provision.node.History; import com.yahoo.vespa.hosted.provision.node.Status; -import com.yahoo.vespa.hosted.provision.testutils.FlavorConfigBuilder; 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; @@ -36,173 +27,151 @@ 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.Matchers.anyString; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * @author freva */ +@RunWith(Enclosed.class) public class NodeRetirerTest { - private final List<Flavor> flavors = makeFlavors(5); - private final Map<String, Node> nodesByHostname = new HashMap<>(); - private final NodeRepository nodeRepository = mock(NodeRepository.class); - private final Zone zone = new Zone(Environment.prod, RegionName.from("us-north-3")); - - @Test - public void testRetireUnallocatedNodes() { - addNodesByFlavor(Node.State.ready, 5, 3, 77, 47); - deployApp("vespa", "calendar", 0, 3); - deployApp("vespa", "notes", 2, 12); - deployApp("sports", "results", 2, 7); - deployApp("search", "images", 3, 6); - - RetirementPolicy policy = node -> node.ipAddresses().equals(Collections.singleton("::1")); - NodeRetirer retirer = new NodeRetirer(nodeRepository, zone, Duration.ofDays(1), policy); - Map<Flavor, Long> expected = expectedCountsByFlavor(1, 3, 56, 40); - Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(nodeRepository.getNodes()); - assertEquals(expected, actual); - - // Not all nodes that we wanted to retire could be retired now (Not enough spare nodes) - assertFalse(retirer.retireUnallocated()); - Map<Flavor, Long> parkedCountsByFlavor = nodesByHostname.values().stream() - .filter(node -> node.state() == Node.State.parked) - .collect(Collectors.groupingBy(Node::flavor, Collectors.counting())); - assertEquals(expected, parkedCountsByFlavor); - - expected = expectedCountsByFlavor(0, 0, 0, 0); - expected.remove(flavors.get(1)); // Flavor-1 has 0 ready nodes, so just remove it to easily compare maps - actual = retirer.getNumberSpareReadyNodesByFlavor(nodeRepository.getNodes()); - assertEquals(expected, actual); - - // Lets remove all the currently parked nodes and reprovision them different IP address - nodesByHostname.entrySet().stream().filter(entry -> entry.getValue().state() == Node.State.parked).forEach(entry -> - updateNode(entry.getKey(), entry.getValue().flavor(), Node.State.ready, Optional.empty(), Collections.singleton("::2"))); - - expected = expectedCountsByFlavor(1, 3, 56, 40); - actual = retirer.getNumberSpareReadyNodesByFlavor(nodeRepository.getNodes()); - assertEquals(expected, actual); - - // The remaining nodes we wanted to retire has been retired - assertTrue(retirer.retireUnallocated()); - parkedCountsByFlavor = nodesByHostname.values().stream() - .filter(node -> node.state() == Node.State.parked) - .collect(Collectors.groupingBy(Node::flavor, Collectors.counting())); - expected = expectedCountsByFlavor(1, 0, 2, 1); - expected.remove(flavors.get(1)); // Flavor-1 already had all its nodes retired & reprovisioned, so just remove it to easily compare maps - assertEquals(expected, parkedCountsByFlavor); - } - - @Test - public void testGetNumberSpareNodesWithNoActiveNodes() { - addNodesByFlavor(Node.State.ready, 5, 3, 77); - NodeRetirer retirer = new NodeRetirer(nodeRepository, zone, Duration.ofDays(1), node -> false); - Map<Flavor, Long> expected = expectedCountsByFlavor(5, 3, 77); - Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(nodeRepository.getNodes()); - assertEquals(expected, actual); + public static class FullNodeRepositoryTester { + + @Test + public void testRetireUnallocatedNodes() { + NodeRetirerTester tester = new NodeRetirerTester(NodeRetirerTester.makeFlavors(5)); + RetirementPolicy policy = node -> node.ipAddresses().equals(Collections.singleton("::1")); + NodeRetirer retirer = new NodeRetirer(tester.nodeRepository, NodeRetirerTester.zone, Duration.ofDays(1), new JobControl(tester.nodeRepository.database()), policy); + + tester.createReadyNodesByFlavor(5, 3, 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); + + Map<Flavor, Long> expected = tester.expectedCountsByFlavor(1, 3, 56, 40); + Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(tester.nodeRepository.getNodes()); + assertEquals(expected, actual); + + // Not all nodes that we wanted to retire could be retired now (Not enough spare nodes) + assertFalse(retirer.retireUnallocated()); + Map<Flavor, Long> parkedCountsByFlavor = tester.nodeRepository.getNodes(Node.State.parked).stream() + .collect(Collectors.groupingBy(Node::flavor, Collectors.counting())); + assertEquals(expected, parkedCountsByFlavor); + + expected = tester.expectedCountsByFlavor(0, -1, 0, 0); + actual = retirer.getNumberSpareReadyNodesByFlavor(tester.nodeRepository.getNodes()); + assertEquals(expected, actual); + + // Lets change parked nodes IP address and set it back to ready + tester.nodeRepository.getNodes(Node.State.parked) + .forEach(node -> { + tester.nodeRepository.write(node.withIpAddresses(Collections.singleton("::2"))); + tester.nodeRepository.setDirty(node.hostname()); + tester.nodeRepository.setReady(node.hostname()); + }); + + expected = tester.expectedCountsByFlavor(1, 3, 56, 40); + actual = retirer.getNumberSpareReadyNodesByFlavor(tester.nodeRepository.getNodes()); + assertEquals(expected, actual); + + // The remaining nodes we wanted to retire has been retired + assertTrue(retirer.retireUnallocated()); + parkedCountsByFlavor = tester.nodeRepository.getNodes(Node.State.parked).stream() + .collect(Collectors.groupingBy(Node::flavor, Collectors.counting())); + expected = tester.expectedCountsByFlavor(1, -1, 2, 1); + assertEquals(expected, parkedCountsByFlavor); + } } - @Test - public void testGetNumberSpareNodesWithActiveNodes() { - addNodesByFlavor(Node.State.ready, 5, 3, 77, 47); - addNodesByFlavor(Node.State.active, 0, 10, 2, 230, 137); - NodeRetirer retirer = new NodeRetirer(nodeRepository, zone, Duration.ofDays(1), node -> false); - Map<Flavor, Long> expected = expectedCountsByFlavor(5, 2, 76, 24); - Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(nodeRepository.getNodes()); - assertEquals(expected, actual); - } + /** + * For testing methods that require minimal node repository and flavor setup + */ + public static class HelperMethodsTester { + private final List<Flavor> flavors = NodeRetirerTester.makeFlavors(5).getFlavors(); + private final List<Node> nodes = new ArrayList<>(); + private final NodeRetirer retirer = mock(NodeRetirer.class); - @Test - public void testGetNumSpareNodes() { - NodeRetirer retirer = new NodeRetirer(nodeRepository, zone, Duration.ofDays(1), node -> false); - assertEquals(retirer.getNumSpareNodes(0, 0), 0L); - assertEquals(retirer.getNumSpareNodes(0, 1), 1L); - assertEquals(retirer.getNumSpareNodes(0, 100), 100L); - - assertEquals(retirer.getNumSpareNodes(1, 0), 0L); - assertEquals(retirer.getNumSpareNodes(1, 1), 0L); - assertEquals(retirer.getNumSpareNodes(1, 2), 1L); - assertEquals(retirer.getNumSpareNodes(43, 23), 18L); - } + @Test + public void testGetNumberSpareNodesWithNoActiveNodes() { + addNodesByFlavor(Node.State.ready, 5, 3, 77); + Map<Flavor, Long> expected = expectedCountsByFlavor(5, 3, 77); + Map<Flavor, Long> actual = retirer.getNumberSpareReadyNodesByFlavor(nodes); + assertEquals(expected, actual); + } - @Before - public void setup() { - when(nodeRepository.getNodes()).thenAnswer(invoc -> new ArrayList<>(nodesByHostname.values())); - when(nodeRepository.lockUnallocated()).thenReturn(null); - when(nodeRepository.park(anyString(), eq(Agent.NodeRetirer), any())).then(invocation -> { - Object[] args = invocation.getArguments(); - String hostname = (String) args[0]; - Node nodeToPark = Optional.ofNullable(nodesByHostname.get(hostname)) - .orElseThrow(() -> new RuntimeException("Could not find node with hostname " + hostname)); - if (nodeToPark.state() == Node.State.parked) throw new RuntimeException("Node already parked!"); - - updateNode(nodeToPark.hostname(), nodeToPark.flavor(), Node.State.parked, nodeToPark.allocation(), nodeToPark.ipAddresses()); - return null; - }); - } + @Test + public void testGetNumberSpareNodesWithActiveNodes() { + addNodesByFlavor(Node.State.ready, 5, 3, 77, 47); + addNodesByFlavor(Node.State.active, 0, 10, 2, 230, 137); - 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]); + Map<Flavor, Long> expected = expectedCountsByFlavor(5, 2, 76, 24); + 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(); } - 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 = nodesByHostname.size(); - updateNode("host-" + id + ".yahoo.com", flavor, state, Optional.empty(), Collections.singleton("::1")); + 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 deployApp(String tenantName, String applicationName, int flavorId, int numNodes) { - Flavor flavor = flavors.get(flavorId); - List<Node> freeNodes = nodeRepository.getNodes().stream() - .filter(node -> node.state() == Node.State.ready) - .filter(node -> node.flavor() == flavor) - .collect(Collectors.toList()); - - if (freeNodes.size() < numNodes) throw new IllegalArgumentException( - "Not enough nodes to deploy " + applicationName + " on " + flavor + " needed " + numNodes + " but has " + freeNodes.size()); - - ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default"); - ClusterSpec cluster = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("test"), Version.fromString("6.99")); - Allocation allocation = new Allocation(applicationId, ClusterMembership.from(cluster, 0), new Generation(0, 0), false); - freeNodes.stream().limit(numNodes).forEach(node -> { - updateNode(node.hostname(), node.flavor(), Node.State.active, Optional.of(allocation), node.ipAddresses()); - }); - } + 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 List<Flavor> 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); + private Node createNode(String hostname, Flavor flavor, Node.State state, Optional<Allocation> allocation, Set<String> ipAddresses) { + return new Node( + UUID.randomUUID().toString(), + ipAddresses, + hostname, + Optional.empty(), + flavor, + Status.initial(), + state, + allocation, + History.empty(), + NodeType.tenant); } - return flavorConfigBuilder.build().flavor().stream().map(Flavor::new).collect(Collectors.toList()); } - private void updateNode(String hostname, Flavor flavor, Node.State state, Optional<Allocation> allocation, Set<String> ipAddresses) { - Node node = new Node( - UUID.randomUUID().toString(), - ipAddresses, - hostname, - Optional.empty(), - flavor, - Status.initial(), - state, - allocation, - History.empty(), - NodeType.tenant); - - nodesByHostname.put(hostname, node); + /** + * 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), 1L); + assertEquals(retirer.getNumSpareNodes(0, 100), 100L); + + assertEquals(retirer.getNumSpareNodes(1, 0), 0L); + assertEquals(retirer.getNumSpareNodes(1, 1), 0L); + assertEquals(retirer.getNumSpareNodes(1, 2), 1L); + assertEquals(retirer.getNumSpareNodes(43, 23), 18L); + } } } 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 new file mode 100644 index 00000000000..635e7ac7d10 --- /dev/null +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRetirerTester.java @@ -0,0 +1,125 @@ +// 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.maintenance; + +import com.yahoo.component.Version; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Capacity; +import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.ClusterSpec; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.Flavor; +import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.NodeFlavors; +import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Zone; +import com.yahoo.test.ManualClock; +import com.yahoo.transaction.NestedTransaction; +import com.yahoo.vespa.curator.Curator; +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.provisioning.NodeRepositoryProvisioner; +import com.yahoo.vespa.hosted.provision.testutils.FlavorConfigBuilder; +import com.yahoo.vespa.hosted.provision.testutils.MockDeployer; +import com.yahoo.vespa.hosted.provision.testutils.MockNameResolver; +import com.yahoo.vespa.hosted.provision.testutils.OrchestratorMock; +import com.yahoo.vespa.hosted.provision.testutils.ServiceMonitorStub; +import com.yahoo.vespa.orchestrator.Orchestrator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + + +/** + * @author freva + */ +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 NodeRepository nodeRepository; + public ServiceMonitorStub serviceMonitor; + public MockDeployer deployer; + private final Orchestrator orchestrator; + private final NodeRepositoryProvisioner provisioner; + private final Curator curator; + private final List<Flavor> flavors; + private int nextNodeId = 0; + + public NodeRetirerTester(NodeFlavors nodeFlavors) { + clock = new ManualClock(); + curator = new MockCurator(); + nodeRepository = new NodeRepository(nodeFlavors, curator, clock, zone, new MockNameResolver().mockAnyLookup()); + provisioner = new NodeRepositoryProvisioner(nodeRepository, nodeFlavors, zone); + orchestrator = new OrchestratorMock(); + deployer = new MockDeployer(provisioner, Collections.emptyMap()); + serviceMonitor = new ServiceMonitorStub(Collections.emptyMap(), nodeRepository); + flavors = nodeFlavors.getFlavors(); + } + + public void suspend(ApplicationId app) { + try { + orchestrator.suspend(app); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void createReadyNodesByFlavor(int... nums) { + List<Node> nodes = new ArrayList<>(); + for (int i = 0; i < nums.length; i++) { + Flavor flavor = flavors.get(i); + for (int j = 0; j < nums[i]; j++) { + int id = nextNodeId++; + nodes.add(nodeRepository.createNode("node" + id, "host" + id + ".test.yahoo.com", + Collections.singleton("::1"), Optional.empty(), flavor, NodeType.tenant)); + } + } + + nodes = nodeRepository.addNodes(nodes); + nodes = nodeRepository.setDirty(nodes); + nodeRepository.setReady(nodes); + } + + public void deployApp(String tenantName, String applicationName, int flavorId, int numNodes) { + Flavor flavor = flavors.get(flavorId); + + ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default"); + ClusterSpec cluster = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("test"), Version.fromString("6.99")); + Capacity capacity = Capacity.fromNodeCount(numNodes, flavor.name()); + + activate(applicationId, cluster, capacity); + } + + private void activate(ApplicationId applicationId, ClusterSpec cluster, Capacity capacity) { + List<HostSpec> hosts = provisioner.prepare(applicationId, cluster, capacity, 1, null); + NestedTransaction transaction = new NestedTransaction().add(new CuratorTransaction(curator)); + provisioner.activate(transaction, applicationId, hosts); + transaction.commit(); + } + + public 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; + Flavor flavor = flavors.get(i); + countsByFlavor.put(flavor, nums[i]); + } + return countsByFlavor; + } + + 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()); + } +} |