diff options
Diffstat (limited to 'node-repository/src/main')
29 files changed, 345 insertions, 465 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index 0f3a87ff585..80333dcd2d4 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.hosted.provision.node.Allocation; import com.yahoo.vespa.hosted.provision.node.Flavor; import com.yahoo.vespa.hosted.provision.node.History; @@ -29,7 +30,7 @@ public final class Node { private final Flavor flavor; private final Status status; private final State state; - private final Type type; + private final NodeType type; /** Record of the last event of each type happening to this node */ private final History history; @@ -38,20 +39,20 @@ public final class Node { private Optional<Allocation> allocation; /** Creates a node in the initial state (provisioned) */ - public static Node create(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, Type type) { + public static Node create(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, NodeType type) { return new Node(openStackId, hostname, parentHostname, flavor, Status.initial(), State.provisioned, Optional.empty(), History.empty(), type); } /** Do not use. Construct nodes by calling {@link NodeRepository#createNode} */ public Node(String openStackId, String hostname, Optional<String> parentHostname, - Flavor flavor, Status status, State state, Allocation allocation, History history, Type type) { + Flavor flavor, Status status, State state, Allocation allocation, History history, NodeType type) { this(openStackId, hostname, parentHostname, flavor, status, state, Optional.of(allocation), history, type); } public Node(String openStackId, String hostname, Optional<String> parentHostname, Flavor flavor, Status status, State state, Optional<Allocation> allocation, - History history, Type type) { + History history, NodeType type) { Objects.requireNonNull(openStackId, "A node must have an openstack id"); Objects.requireNonNull(hostname, "A node must have a hostname"); Objects.requireNonNull(parentHostname, "A null parentHostname is not permitted."); @@ -93,14 +94,14 @@ public final class Node { /** Returns the flavor of this node */ public Flavor flavor() { return flavor; } - /** Returns the known information about the nodes ephemeral status */ + /** Returns the known information about the node's ephemeral status */ public Status status() { return status; } /** Returns the current state of this node (in the node state machine) */ public State state() { return state; } /** Returns the type of this node */ - public Type type() { return type; } + public NodeType type() { return type; } /** Returns the current allocation of this, if any */ public Optional<Allocation> allocation() { return allocation; } @@ -130,7 +131,7 @@ public final class Node { return with(allocation.get().unretire()); } - /** Returns a copy of this with the current generation set to generation */ + /** Returns a copy of this with the current restart generation set to generation */ public Node withRestart(Generation generation) { final Optional<Allocation> allocation = this.allocation; if ( ! allocation.isPresent()) @@ -145,7 +146,7 @@ public final class Node { } /** Returns a node with the type assigned to the given value */ - public Node with(Type type) { + public Node with(NodeType type) { return new Node(openStackId, hostname, parentHostname, flavor, status, state, allocation, history, type); } @@ -154,7 +155,7 @@ public final class Node { return new Node(openStackId, hostname, parentHostname, flavor, status, state, allocation, history, type); } - /** Returns a copy of this with the current generation set to generation */ + /** Returns a copy of this with the current reboot generation set to generation */ public Node withReboot(Generation generation) { return new Node(openStackId, hostname, parentHostname, flavor, status.withReboot(generation), state, allocation, history, type); @@ -217,7 +218,7 @@ public final class Node { public enum State { - /** This node has been requested (from OpenStack) but is not yet read for use */ + /** This node has been requested (from OpenStack) but is not yet ready for use */ provisioned, /** This node is free and ready for use */ @@ -251,17 +252,4 @@ public final class Node { } } - public enum Type { - - /** A host of a set of (docker) tenant nodes */ - host, - - /** Nodes running the shared proxy layer */ - proxy, - - /** A node to be assigned to a tenant to run application workloads */ - tenant - - } - } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java index 606a5914e11..eee0eae4e83 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java @@ -5,6 +5,7 @@ import com.google.inject.Inject; import com.yahoo.collections.ListMap; import com.yahoo.component.AbstractComponent; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.NodeType; import com.yahoo.transaction.Mutex; import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.curator.Curator; @@ -91,13 +92,22 @@ public class NodeRepository extends AbstractComponent { } /** + * Returns all nodes in any of the given states. + * + * @param inState the states to return nodes from. If no states are given, all nodes of the given type are returned + * @return the node, or empty if it was not found in any of the given states + */ + public List<Node> getNodes(Node.State ... inState) { + return zkClient.getNodes(inState).stream().collect(Collectors.toList()); + } + /** * Finds and returns the nodes of the given type in any of the given states. * * @param type the node type to return * @param inState the states to return nodes from. If no states are given, all nodes of the given type are returned * @return the node, or empty if it was not found in any of the given states */ - public List<Node> getNodes(Node.Type type, Node.State ... inState) { + public List<Node> getNodes(NodeType type, Node.State ... inState) { return zkClient.getNodes(inState).stream().filter(node -> node.type().equals(type)).collect(Collectors.toList()); } public List<Node> getNodes(ApplicationId id, Node.State ... inState) { return zkClient.getNodes(id, inState); } @@ -114,7 +124,7 @@ public class NodeRepository extends AbstractComponent { /** Creates a new node object, without adding it to the node repo */ public Node createNode(String openStackId, String hostname, Optional<String> parentHostname, - Flavor flavor, Node.Type type) { + Flavor flavor, NodeType type) { return Node.create(openStackId, hostname, parentHostname, flavor, type); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java index 14305692664..422eee820db 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/assimilate/PopulateClient.java @@ -95,7 +95,7 @@ public class PopulateClient { Node.State.active, Optional.empty() /* Allocation */, History.empty(), - Node.Type.tenant) // History + NodeType.tenant) // History .allocate( ApplicationId.from( diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java index ce7ae429c40..32478473111 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ApplicationMaintainer.java @@ -4,9 +4,9 @@ package com.yahoo.vespa.hosted.provision.maintenance; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Deployer; import com.yahoo.config.provision.Deployment; +import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; -import com.yahoo.yolean.Exceptions; import java.time.Duration; import java.util.Optional; @@ -35,7 +35,7 @@ public class ApplicationMaintainer extends Maintainer { @Override protected void maintain() { Set<ApplicationId> applications = - nodeRepository().getNodes(Node.Type.tenant, Node.State.active).stream().map(node -> node.allocation().get().owner()).collect(Collectors.toSet()); + nodeRepository().getNodes(Node.State.active).stream().map(node -> node.allocation().get().owner()).collect(Collectors.toSet()); for (ApplicationId application : applications) { try { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java index 495ff3b756f..5b6cc3b11a3 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/Expirer.java @@ -1,6 +1,7 @@ // 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.config.provision.NodeType; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.History; @@ -47,7 +48,7 @@ public abstract class Expirer extends Maintainer { @Override protected void maintain() { List<Node> expired = new ArrayList<>(); - for (Node node : nodeRepository().getNodes(Node.Type.tenant, fromState)) { + for (Node node : nodeRepository().getNodes(fromState)) { Optional<History.Event> event = node.history().event(eventType); if (event.isPresent() && event.get().at().plus(expiryTime).isBefore(clock.instant())) expired.add(node); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java index 4b16c0947cb..f194d9d53fd 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.maintenance; import com.yahoo.config.provision.Deployer; import com.yahoo.config.provision.Deployment; import com.yahoo.config.provision.HostLivenessTracker; +import com.yahoo.config.provision.NodeType; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ServiceCluster; @@ -86,7 +87,7 @@ public class NodeFailer extends Maintainer { // Active nodes for (Node node : determineActiveNodeDownStatus()) { Instant graceTimeEnd = node.history().event(History.Event.Type.down).get().at().plus(downTimeLimit); - if (graceTimeEnd.isBefore(clock.instant()) && ! applicationSuspended(node)) + if (graceTimeEnd.isBefore(clock.instant()) && ! applicationSuspended(node) && failAllowedFor(node.type())) failActive(node); } } @@ -95,7 +96,7 @@ public class NodeFailer extends Maintainer { // Update node last request events through ZooKeeper to collect request to all config servers. // We do this here ("lazily") to avoid writing to zk for each config request. try (Mutex lock = nodeRepository().lockUnallocated()) { - for (Node node : nodeRepository().getNodes(Node.Type.tenant, Node.State.ready)) { + for (Node node : nodeRepository().getNodes(Node.State.ready)) { Optional<Instant> lastLocalRequest = hostLivenessTracker.lastRequestFrom(node.hostname()); if ( ! lastLocalRequest.isPresent()) continue; @@ -118,7 +119,7 @@ public class NodeFailer extends Maintainer { // Add 10 minutes to the down time limit to allow nodes to make a request that infrequently. Instant oldestAcceptableRequestTime = clock.instant().minus(downTimeLimit).minus(nodeRequestInterval); - return nodeRepository().getNodes(Node.Type.tenant, Node.State.ready).stream() + return nodeRepository().getNodes(Node.State.ready).stream() .filter(node -> wasMadeReadyBefore(oldestAcceptableRequestTime, node)) .filter(node -> ! hasRecordedRequestAfter(oldestAcceptableRequestTime, node)) .collect(Collectors.toList()); @@ -137,7 +138,7 @@ public class NodeFailer extends Maintainer { } private List<Node> readyNodesWithHardwareFailure() { - return nodeRepository().getNodes(Node.Type.tenant, Node.State.ready).stream() + return nodeRepository().getNodes(Node.State.ready).stream() .filter(node -> node.status().hardwareFailure().isPresent()) .collect(Collectors.toList()); } @@ -153,6 +154,17 @@ public class NodeFailer extends Maintainer { } /** + * We can attempt to fail any number of *tenant* nodes because the operation will not be effected unless + * the node is replaced. + * However, nodes of other types are not replaced (because all of the type are used by a single application), + * so we only allow one to be in failed at any point in time to protect against runaway failing. + */ + private boolean failAllowedFor(NodeType nodeType) { + if (nodeType == NodeType.tenant) return true; + return nodeRepository().getNodes(nodeType, Node.State.failed).size() == 0; + } + + /** * If the node is positively DOWN, and there is no "down" history record, we add it. * If the node is positively UP we remove any "down" history record. * @@ -164,7 +176,7 @@ public class NodeFailer extends Maintainer { for (ServiceCluster<ServiceMonitorStatus> cluster : application.serviceClusters()) { for (ServiceInstance<ServiceMonitorStatus> service : cluster.serviceInstances()) { Optional<Node> node = nodeRepository().getNode(service.hostName().s(), Node.State.active); - if ( ! node.isPresent()) continue; // we also get status from infrastructure nodes, which are not in the repo + if ( ! node.isPresent()) continue; // we also get status from infrastructure nodes, which are not in the repo. TODO: remove when proxy nodes are in node repo everywhere if (service.serviceStatus().equals(ServiceMonitorStatus.DOWN)) downNodes.add(recordAsDown(node.get())); 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 3e6881b912b..fa3bdeea776 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 @@ -105,7 +105,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent { redeployFrequency = Duration.ofMinutes(30); zooKeeperAccessMaintenanceInterval = Duration.ofSeconds(10); reservationExpiry = Duration.ofMinutes(10); // Need to be long enough for deployment to be finished for all config model versions - inactiveExpiry = Duration.ofMinutes(1); + inactiveExpiry = Duration.ofSeconds(2); // support interactive wipe start over retiredExpiry = Duration.ofMinutes(1); failedExpiry = Duration.ofMinutes(10); dirtyExpiry = Duration.ofMinutes(30); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java index 085e3e4dac8..d558555512f 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ZooKeeperAccessMaintainer.java @@ -1,5 +1,6 @@ package com.yahoo.vespa.hosted.provision.maintenance; +import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.curator.Curator; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; @@ -31,9 +32,9 @@ public class ZooKeeperAccessMaintainer extends Maintainer { protected void maintain() { StringBuilder hostList = new StringBuilder(); - for (Node node : nodeRepository().getNodes(Node.Type.tenant)) + for (Node node : nodeRepository().getNodes(NodeType.tenant)) hostList.append(node.hostname()).append(","); - for (Node node : nodeRepository().getNodes(Node.Type.proxy)) + for (Node node : nodeRepository().getNodes(NodeType.proxy)) hostList.append(node.hostname()).append(","); for (String hostPort : curator.connectionSpec().split(",")) hostList.append(hostPort.split(":")[0]).append(","); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java index b1e0df929f4..18f4765fa03 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/monitoring/ProvisionMetrics.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.provision.monitoring; import com.yahoo.component.AbstractComponent; +import com.yahoo.config.provision.NodeType; import com.yahoo.jdisc.Metric; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.provision.Node; @@ -50,10 +51,11 @@ public class ProvisionMetrics extends AbstractComponent { log.log(LogLevel.DEBUG, "Running provision metrics task"); try { for (Node.State state : Node.State.values()) - metric.set("hostedVespa." + state.name() + "Hosts", nodeRepository.getNodes(Node.Type.tenant, state).size(), null); + metric.set("hostedVespa." + state.name() + "Hosts", nodeRepository.getNodes(NodeType.tenant, state).size(), null); } catch (RuntimeException e) { log.log(LogLevel.INFO, "Failed gathering metrics data: " + e.getMessage()); } } } + } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java index 0893ca75f92..1039beea7c0 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/Flavor.java @@ -17,6 +17,7 @@ public class Flavor { private final String name; private final int cost; + private final boolean isStock; private final Type type; private final double minCpuCores; private final double minMainMemoryAvailableGb; @@ -32,6 +33,7 @@ public class Flavor { this.name = flavorConfig.name(); this.replacesFlavors = new ArrayList<>(); this.cost = flavorConfig.cost(); + this.isStock = flavorConfig.stock(); this.type = Type.valueOf(flavorConfig.environment()); this.minCpuCores = flavorConfig.minCpuCores(); this.minMainMemoryAvailableGb = flavorConfig.minMainMemoryAvailableGb(); @@ -39,6 +41,7 @@ public class Flavor { this.description = flavorConfig.description(); } + /** Returns the unique identity of this flavor */ public String name() { return name; } /** @@ -47,26 +50,18 @@ public class Flavor { * @return Monthly cost in USD */ public int cost() { return cost; } + + public boolean isStock() { return isStock; } - public double getMinMainMemoryAvailableGb() { - return minMainMemoryAvailableGb; - } + public double getMinMainMemoryAvailableGb() { return minMainMemoryAvailableGb; } - public double getMinDiskAvailableGb() { - return minDiskAvailableGb; - } + public double getMinDiskAvailableGb() { return minDiskAvailableGb; } - public double getMinCpuCores() { - return minCpuCores; - } + public double getMinCpuCores() { return minCpuCores; } - public String getDescription() { - return description; - } + public String getDescription() { return description; } - public Type getType() { - return type; - } + public Type getType() { return type; } /** * Returns the canonical name of this flavor - which is the name which should be used as an interface to users. @@ -78,11 +73,16 @@ public class Flavor { * * The logic is that we can use this to capture the gritty details of configurations in exact flavor names * but also encourage users to refer to them by a common name by letting such flavor variants declare that they - * replace the canonical name we want. However, if a node replaces multiple names, it means that a former - * flavor distinction has become obsolete so this name becomes one of the canonical names users should refer to. + * replace the canonical name we want. However, if a node replaces multiple names, we have no basis for choosing one + * of them as the canonical, so we return the current as canonical. */ public String canonicalName() { - return replacesFlavors.size() == 1 ? replacesFlavors.get(0).canonicalName() : name; + return isCanonical() ? name : replacesFlavors.get(0).canonicalName(); + } + + /** Returns whether this is a canonical flavor */ + public boolean isCanonical() { + return replacesFlavors.size() != 1; } /** diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java index 7fb3abb5b8e..d85347847ae 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/NodeSerializer.java @@ -6,6 +6,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.TenantName; import com.yahoo.slime.ArrayTraverser; import com.yahoo.slime.Cursor; @@ -279,15 +280,15 @@ public class NodeSerializer { throw new IllegalArgumentException("Serialized form of '" + agent + "' not defined"); } - private Node.Type nodeTypeFromString(String typeString) { + private NodeType nodeTypeFromString(String typeString) { switch (typeString) { - case "tenant" : return Node.Type.tenant; - case "host" : return Node.Type.host; - case "proxy" : return Node.Type.proxy; + case "tenant" : return NodeType.tenant; + case "host" : return NodeType.host; + case "proxy" : return NodeType.proxy; default : throw new IllegalArgumentException("Unknown node type '" + typeString + "'"); } } - private String toString(Node.Type type) { + private String toString(NodeType type) { switch (type) { case tenant: return "tenant"; case host: return "host"; diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java index 6665833c1a2..a759a8fca37 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/CapacityPolicies.java @@ -31,7 +31,7 @@ public class CapacityPolicies { switch(zone.environment()) { case dev : case test : return 1; - case perf : return Math.min(requestedCapacity.nodeCount(), 10); // TODO: Decrease to 3 when isRequired is implemented + case perf : return Math.min(requestedCapacity.nodeCount(), 3); case staging: return requestedNodes <= 1 ? requestedNodes : Math.max(2, requestedNodes / 10); case prod : return ensureRedundancy(requestedCapacity.nodeCount()); default : throw new IllegalArgumentException("Unsupported environment " + zone.environment()); @@ -53,7 +53,7 @@ public class CapacityPolicies { /** * Throw if the node count is 1 - + * * @return the argument node count * @throws IllegalArgumentException if only one node is requested */ diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java index 583111b9d65..ea205a15040 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java @@ -10,7 +10,6 @@ import com.yahoo.lang.MutableInteger; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeRepository; -import com.yahoo.vespa.hosted.provision.node.Flavor; import java.time.Clock; import java.util.ArrayList; @@ -46,8 +45,7 @@ class GroupPreparer { * * @param application the application we are allocating to * @param cluster the cluster and group we are allocating to - * @param nodeCount the desired number of nodes to return - * @param flavor the desired flavor of those nodes + * @param requestedNodes a specification of the requested nodes * @param surplusActiveNodes currently active nodes which are available to be assigned to this group. * This method will remove from this list if it finds it needs additional nodes * @param highestIndex the current highest node index among all active nodes in this cluster. @@ -57,58 +55,71 @@ class GroupPreparer { // Note: This operation may make persisted changes to the set of reserved and inactive nodes, // but it may not change the set of active nodes, as the active nodes must stay in sync with the // active config model which is changed on activate - public List<Node> prepare(ApplicationId application, ClusterSpec cluster, int nodeCount, Flavor flavor, List<Node> surplusActiveNodes, MutableInteger highestIndex) { + public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, + List<Node> surplusActiveNodes, MutableInteger highestIndex) { try (Mutex lock = nodeRepository.lock(application)) { - NodeList nodeList = new NodeList(application, cluster, nodeCount, flavor, highestIndex); + NodeList nodeList = new NodeList(application, cluster, requestedNodes, highestIndex); // Use active nodes nodeList.offer(nodeRepository.getNodes(application, Node.State.active), !canChangeGroup); - if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes); + if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes); // Use active nodes from other groups that will otherwise be retired - List<Node> accepted = nodeList.offer(sortNodeListByCost(surplusActiveNodes), canChangeGroup); + List<Node> accepted = nodeList.offer(prioritizeNodes(surplusActiveNodes, requestedNodes), canChangeGroup); surplusActiveNodes.removeAll(accepted); - if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes); + if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes); // Use previously reserved nodes nodeList.offer(nodeRepository.getNodes(application, Node.State.reserved), !canChangeGroup); - if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes); + if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes); // Use inactive nodes - accepted = nodeList.offer(sortNodeListByCost(nodeRepository.getNodes(application, Node.State.inactive)), !canChangeGroup); + accepted = nodeList.offer(prioritizeNodes(nodeRepository.getNodes(application, Node.State.inactive), requestedNodes), !canChangeGroup); nodeList.update(nodeRepository.reserve(accepted)); - if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes); + if (nodeList.saturated()) return nodeList.finalNodes(surplusActiveNodes); // Use new, ready nodes. Lock ready pool to ensure that nodes are not grabbed by others. try (Mutex readyLock = nodeRepository.lockUnallocated()) { - List<Node> readyNodes = nodeRepository.getNodes(Node.Type.tenant, Node.State.ready); - accepted = nodeList.offer(stripeOverHosts(sortNodeListByCost(readyNodes)), !canChangeGroup); + List<Node> readyNodes = nodeRepository.getNodes(requestedNodes.type(), Node.State.ready); + accepted = nodeList.offer(stripeOverHosts(prioritizeNodes(readyNodes, requestedNodes)), !canChangeGroup); nodeList.update(nodeRepository.reserve(accepted)); } - if (nodeList.satisfied()) return nodeList.finalNodes(surplusActiveNodes); - if (nodeList.whatAboutUsingRetiredNodes()) { - throw new OutOfCapacityException("Could not satisfy request for " + nodeCount + - " nodes of " + flavor + " for " + cluster + + if (nodeList.fullfilled()) return nodeList.finalNodes(surplusActiveNodes); + + // Could not be fulfilled + if (nodeList.wouldBeFulfilledWithRetiredNodes()) + throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster + " because we want to retire existing nodes."); - } - if (nodeList.whatAboutUsingVMs()) { - throw new OutOfCapacityException("Could not satisfy request for " + nodeCount + - " nodes of " + flavor + " for " + cluster + + else if (nodeList.wouldBeFulfilledWithClashingParentHost()) + throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster + " because too many have same parentHost."); - } - throw new OutOfCapacityException("Could not satisfy request for " + nodeCount + - " nodes of " + flavor + " for " + cluster + "."); + else + throw new OutOfCapacityException("Could not satisfy " + requestedNodes + " for " + cluster + "."); } } - /** Sort nodes according to their cost, and if the cost is equal, sort by hostname (to get stable tests) */ - private List<Node> sortNodeListByCost(List<Node> nodeList) { - Collections.sort(nodeList, (n1, n2) -> ComparisonChain.start() - .compare(n1.flavor().cost(), n2.flavor().cost()) - .compare(n1.hostname(), n2.hostname()) - .result() - ); + /** + * Returns the node list in prioritized order, where the nodes we would most prefer the application + * to use comes first + */ + private List<Node> prioritizeNodes(List<Node> nodeList, NodeSpec nodeSpec) { + if ( nodeSpec.specifiesNonStockFlavor()) { // sort by exact before inexact flavor match, increasing cost, hostname + Collections.sort(nodeList, (n1, n2) -> ComparisonChain.start() + .compareTrueFirst(nodeSpec.matchesExactly(n1.flavor()), nodeSpec.matchesExactly(n2.flavor())) + .compare(n1.flavor().cost(), n2.flavor().cost()) + .compare(n1.hostname(), n2.hostname()) + .result() + ); + } + else { // sort by increasing cost, hostname + Collections.sort(nodeList, (n1, n2) -> ComparisonChain.start() + .compareTrueFirst(nodeSpec.matchesExactly(n1.flavor()), nodeSpec.matchesExactly(n1.flavor())) + .compare(n1.flavor().cost(), n2.flavor().cost()) + .compare(n1.hostname(), n2.hostname()) + .result() + ); + } return nodeList; } @@ -159,11 +170,8 @@ class GroupPreparer { /** The cluster this list is for */ private final ClusterSpec cluster; - /** The requested capacity of the list */ - private final int requestedNodes; - - /** The requested node flavor */ - private final Flavor requestedFlavor; + /** The requested nodes of this list */ + private final NodeSpec requestedNodes; /** The nodes this has accepted so far */ private final Set<Node> nodes = new LinkedHashSet<>(); @@ -183,11 +191,10 @@ class GroupPreparer { /** The next membership index to assign to a new node */ private MutableInteger highestIndex; - public NodeList(ApplicationId application, ClusterSpec cluster, int requestedNodes, Flavor requestedFlavor, MutableInteger highestIndex) { + public NodeList(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, MutableInteger highestIndex) { this.application = application; this.cluster = cluster; this.requestedNodes = requestedNodes; - this.requestedFlavor = requestedFlavor; this.highestIndex = highestIndex; } @@ -210,7 +217,7 @@ class GroupPreparer { ClusterMembership membership = offered.allocation().get().membership(); if ( ! offered.allocation().get().owner().equals(application)) continue; // wrong application if ( ! membership.cluster().equalsIgnoringGroup(cluster)) continue; // wrong cluster id/type - if ( (! canChangeGroup || satisfied()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group and we can't or have no reason to change it + if ((! canChangeGroup || saturated()) && ! membership.cluster().group().equals(cluster.group())) continue; // wrong group and we can't or have no reason to change it if ( offered.allocation().get().isRemovable()) continue; // don't accept; causes removal if ( indexes.contains(membership.index())) continue; // duplicate index (just to be sure) @@ -218,10 +225,10 @@ class GroupPreparer { if ( offeredNodeHasParentHostnameAlreadyAccepted(this.nodes, offered)) wantToRetireNode = true; if ( !hasCompatibleFlavor(offered)) wantToRetireNode = true; - if ( ( !satisfied() && hasCompatibleFlavor(offered)) || acceptToRetire(offered) ) + if ((!saturated() && hasCompatibleFlavor(offered)) || acceptToRetire(offered) ) accepted.add(acceptNode(offered, wantToRetireNode)); } - else if (! satisfied() && hasCompatibleFlavor(offered)) { + else if (! saturated() && hasCompatibleFlavor(offered)) { if ( offeredNodeHasParentHostnameAlreadyAccepted(this.nodes, offered)) { ++rejectedWithClashingParentHost; continue; @@ -268,7 +275,7 @@ class GroupPreparer { } private boolean hasCompatibleFlavor(Node node) { - return node.flavor().satisfies(requestedFlavor); + return requestedNodes.isCompatible(node.flavor()); } /** Updates the state of some existing nodes in this list by replacing them by id with the given instances. */ @@ -305,17 +312,22 @@ class GroupPreparer { return node.with(node.allocation().get().with(membership)); } - /** Returns true if we have accepted at least the requested number of nodes of the requested flavor */ - public boolean satisfied() { - return acceptedOfRequestedFlavor >= requestedNodes; + /** Returns true if no more nodes are needed in this list */ + public boolean saturated() { + return requestedNodes.saturatedBy(acceptedOfRequestedFlavor); + } + + /** Returns true if the content of this list is sufficient to meet the request */ + public boolean fullfilled() { + return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor); } - public boolean whatAboutUsingRetiredNodes() { - return acceptedOfRequestedFlavor + wasRetiredJustNow >= requestedNodes; + public boolean wouldBeFulfilledWithRetiredNodes() { + return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor + wasRetiredJustNow); } - public boolean whatAboutUsingVMs() { - return acceptedOfRequestedFlavor + rejectedWithClashingParentHost >= requestedNodes; + public boolean wouldBeFulfilledWithClashingParentHost() { + return requestedNodes.fulfilledBy(acceptedOfRequestedFlavor + rejectedWithClashingParentHost); } /** @@ -329,7 +341,7 @@ class GroupPreparer { */ public List<Node> finalNodes(List<Node> surplusNodes) { long currentRetired = nodes.stream().filter(node -> node.allocation().get().membership().retired()).count(); - long surplus = nodes.size() - requestedNodes - currentRetired; + long surplus = requestedNodes.surplusGiven(nodes.size()) - currentRetired; List<Node> changedNodes = new ArrayList<>(); if (surplus > 0) { // retire until surplus is 0, prefer to retire higher indexes to minimize redistribution diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java index 5924e3fcb18..8e6c9f0c4ee 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeRepositoryProvisioner.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostFilter; import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.ProvisionLogger; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.Zone; @@ -61,19 +62,33 @@ public class NodeRepositoryProvisioner implements Provisioner { * The nodes are ordered by increasing index number. */ @Override - public List<HostSpec> prepare(ApplicationId application, ClusterSpec cluster, Capacity requestedCapacity, int groups, ProvisionLogger logger) { - log.log(LogLevel.DEBUG, () -> "Received deploy prepare request for " + requestedCapacity + " in " + - groups + " groups for application " + application + ", cluster " + cluster); - - Flavor flavor = capacityPolicies.decideFlavor(requestedCapacity, cluster); - int nodeCount = capacityPolicies.decideSize(requestedCapacity); - int effectiveGroups = groups > nodeCount ? nodeCount : groups; // cannot have more groups than nodes + public List<HostSpec> prepare(ApplicationId application, ClusterSpec cluster, Capacity requestedCapacity, + int wantedGroups, ProvisionLogger logger) { + if (cluster.group().isPresent()) throw new IllegalArgumentException("Node requests cannot specify a group"); + if (requestedCapacity.nodeCount() > 0 && requestedCapacity.nodeCount() % wantedGroups != 0) + throw new IllegalArgumentException("Requested " + requestedCapacity.nodeCount() + " nodes in " + wantedGroups + " groups, " + + "which doesn't allow the nodes to be divided evenly into groups"); - if (zone.environment().isManuallyDeployed() && nodeCount < requestedCapacity.nodeCount()) - logger.log(Level.WARNING, "Requested " + requestedCapacity.nodeCount() + " nodes for " + cluster + - ", downscaling to " + nodeCount + " nodes in " + zone.environment()); - - return asSortedHosts(preparer.prepare(application, cluster, nodeCount, flavor, effectiveGroups)); + log.log(LogLevel.DEBUG, () -> "Received deploy prepare request for " + requestedCapacity + " in " + + wantedGroups + " groups for application " + application + ", cluster " + cluster); + + int effectiveGroups; + NodeSpec requestedNodes; + if ( requestedCapacity.type() == NodeType.tenant) { + int nodeCount = capacityPolicies.decideSize(requestedCapacity); + if (zone.environment().isManuallyDeployed() && nodeCount < requestedCapacity.nodeCount()) + logger.log(Level.INFO, "Requested " + requestedCapacity.nodeCount() + " nodes for " + cluster + + ", downscaling to " + nodeCount + " nodes in " + zone.environment()); + Flavor flavor = capacityPolicies.decideFlavor(requestedCapacity, cluster); + effectiveGroups = wantedGroups > nodeCount ? nodeCount : wantedGroups; // cannot have more groups than nodes + requestedNodes = NodeSpec.from(nodeCount, flavor); + } + else { + requestedNodes = NodeSpec.from(requestedCapacity.type()); + effectiveGroups = 1; // type request with multiple groups is not supported + } + + return asSortedHosts(preparer.prepare(application, cluster, requestedNodes, effectiveGroups)); } @Override diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java new file mode 100644 index 00000000000..2ce364daa07 --- /dev/null +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/NodeSpec.java @@ -0,0 +1,128 @@ +package com.yahoo.vespa.hosted.provision.provisioning; + +import com.yahoo.config.provision.NodeType; +import com.yahoo.vespa.hosted.provision.node.Flavor; + +import java.util.Objects; + +/** + * A specification of a set of nodes. + * This reflects that nodes can be requested either by count and flavor or by type, + * and encapsulates the differences in logic between these two cases. + * + * @author bratseth + */ +public interface NodeSpec { + + /** The node type this requests */ + NodeType type(); + + /** Returns whether the given flavor is compatible with this spec */ + boolean isCompatible(Flavor flavor); + + /** Returns whether the given flavor is exactly specified by this node spec */ + boolean matchesExactly(Flavor flavor); + + /** Returns whether this requests a non-stock flavor */ + boolean specifiesNonStockFlavor(); + + /** Returns whether the given node count is sufficient to consider this spec fulfilled to the maximum amount */ + boolean saturatedBy(int count); + + /** Returns whether the given node count is sufficient to fulfill this spec */ + boolean fulfilledBy(int count); + + /** Returns the amount the given count is above the minimum amount needed to fulfill this request */ + int surplusGiven(int count); + + /** Returns a specification of a fraction of all the nodes of this. It is assumed the argument is a valid divisor. */ + NodeSpec fraction(int divisor); + + static NodeSpec from(int nodeCount, Flavor flavor) { + return new CountNodeSpec(nodeCount, flavor); + } + + static NodeSpec from(NodeType type) { + return new TypeNodeSpec(type); + } + + /** A node spec specifying a node count and a flavor */ + class CountNodeSpec implements NodeSpec { + + private final int count; + private final Flavor flavor; + + public CountNodeSpec(int count, Flavor flavor) { + Objects.requireNonNull(flavor, "A flavor must be specified"); + this.count = count; + this.flavor = flavor; + } + + @Override + public NodeType type() { return NodeType.tenant; } + + @Override + public boolean isCompatible(Flavor flavor) { return flavor.satisfies(this.flavor); } + + @Override + public boolean matchesExactly(Flavor flavor) { return flavor.equals(this.flavor); } + + @Override + public boolean specifiesNonStockFlavor() { return ! flavor.isStock(); } + + @Override + public boolean fulfilledBy(int count) { return count >= this.count; } + + @Override + public boolean saturatedBy(int count) { return fulfilledBy(count); } // min=max for count specs + + @Override + public int surplusGiven(int count) { return count - this.count; } + + @Override + public NodeSpec fraction(int divisor) { return new CountNodeSpec(count/divisor, flavor); } + + @Override + public String toString() { return "request for " + count + " nodes of " + flavor; } + + } + + /** A node spec specifying a node type. This will accept all nodes of this type. */ + class TypeNodeSpec implements NodeSpec { + + private final NodeType type; + + public TypeNodeSpec(NodeType type) { + this.type = type; + } + + @Override + public NodeType type() { return type; } + + @Override + public boolean isCompatible(Flavor flavor) { return true; } + + @Override + public boolean matchesExactly(Flavor flavor) { return false; } + + @Override + public boolean specifiesNonStockFlavor() { return false; } + + @Override + public boolean fulfilledBy(int count) { return true; } + + @Override + public boolean saturatedBy(int count) { return false; } + + @Override + public int surplusGiven(int count) { return 0; } + + @Override + public NodeSpec fraction(int divisor) { return this; } + + @Override + public String toString() { return "request for all nodes of type '" + type + "'"; } + + } + +} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java index dfb06321233..d34a91aec77 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java @@ -40,28 +40,15 @@ class Preparer { // Note: This operation may make persisted changes to the set of reserved and inactive nodes, // but it may not change the set of active nodes, as the active nodes must stay in sync with the // active config model which is changed on activate - public List<Node> prepare(ApplicationId application, ClusterSpec cluster, int nodes, Flavor flavor, int wantedGroups) { - // TODO: Encode actual assumptions as we have logic that depends on them: - // - Don't allow a cluster spec specifying an explicit group (and then remove the "targetgroup" parameter to moveToActiveGroup - // - Change group ids to be a 0-based integer index - if (cluster.group().isPresent() && wantedGroups > 1) - throw new IllegalArgumentException("Cannot specify both a particular group and request multiple groups"); - if (nodes > 0 && nodes % wantedGroups != 0) - throw new IllegalArgumentException("Requested " + nodes + " nodes in " + wantedGroups + " groups, " + - "which doesn't allow the nodes to be divided evenly into groups"); - - // no group -> this asks for the entire cluster -> we are free to remove groups we won't need - List<Node> surplusNodes = - cluster.group().isPresent() ? new ArrayList<>() : findNodesInRemovableGroups(application, cluster, wantedGroups); + public List<Node> prepare(ApplicationId application, ClusterSpec cluster, NodeSpec requestedNodes, int wantedGroups) { + List<Node> surplusNodes = findNodesInRemovableGroups(application, cluster, wantedGroups); MutableInteger highestIndex = new MutableInteger(findHighestIndex(application, cluster)); List<Node> acceptedNodes = new ArrayList<>(); for (int groupIndex = 0; groupIndex < wantedGroups; groupIndex++) { - // Generated groups always have contiguous indexes starting from 0 - ClusterSpec clusterGroup = - cluster.group().isPresent() ? cluster : cluster.changeGroup(Optional.of(ClusterSpec.Group.from(groupIndex))); - - List<Node> accepted = groupPreparer.prepare(application, clusterGroup, nodes/wantedGroups, flavor, surplusNodes, highestIndex); + ClusterSpec clusterGroup = cluster.changeGroup(Optional.of(ClusterSpec.Group.from(groupIndex))); + List<Node> accepted = groupPreparer.prepare(application, clusterGroup, + requestedNodes.fraction(wantedGroups), surplusNodes, highestIndex); replace(acceptedNodes, accepted); } moveToActiveGroup(surplusNodes, wantedGroups, cluster.group()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java deleted file mode 100644 index a501e7f5a0a..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ContainersForHost.java +++ /dev/null @@ -1,41 +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.restapi.legacy; - -import java.util.List; - -/** - * Represents the JSON reply for getContainersForHost. - * Serialized by jackson, and therefore uses public fields to avoid writing cruft. - * - * @author tonytv - */ -public class ContainersForHost { - - public List<DockerContainer> dockerContainers; - - public static class DockerContainer { - public String containerHostname; - public String dockerImage; - public String nodeState; - public long wantedRestartGeneration; - public long currentRestartGeneration; - - public DockerContainer( - String containerHostname, - String dockerImage, - String nodeState, - long wantedRestartGeneration, - long currentRestartGeneration) { - this.containerHostname = containerHostname; - this.dockerImage = dockerImage; - this.nodeState = nodeState; - this.wantedRestartGeneration = wantedRestartGeneration; - this.currentRestartGeneration = currentRestartGeneration; - } - } - - public ContainersForHost(List<DockerContainer> dockerContainers) { - this.dockerContainers = dockerContainers; - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java deleted file mode 100644 index 81211f978f7..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/HostInfo.java +++ /dev/null @@ -1,27 +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.restapi.legacy; - -/** - * Value class used to automatically convert to/from JSON. - * - * @author Oyvind Gronnesby - */ -class HostInfo { - - public String hostname; - public String openStackId; - public String flavor; - - public static HostInfo createHostInfo(String hostname, String openStackId, String flavor) { - HostInfo hostInfo = new HostInfo(); - hostInfo.hostname = hostname; - hostInfo.openStackId = openStackId; - hostInfo.flavor = flavor; - return hostInfo; - } - - public String toString(){ - return String.format("%s/%s", openStackId, hostname); - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java deleted file mode 100644 index caef4630544..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionEndpoint.java +++ /dev/null @@ -1,25 +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.restapi.legacy; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; - -/** - * To avoid duplication of URI construction. - * This class should be deleted when there's a provision client configured in services xml. - * @author tonytv - */ -public class ProvisionEndpoint { - - public static final int configServerPort = 19071; - - public static URI provisionUri(String configServerHostName, int port) { - try { - return new URL("http", configServerHostName, port, "/hack/provision").toURI(); - } catch (URISyntaxException | MalformedURLException e) { - throw new IllegalArgumentException("Failed creating provisionUri from " + configServerHostName, e); - } - } -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java deleted file mode 100644 index da55ef9a15d..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionResource.java +++ /dev/null @@ -1,150 +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.restapi.legacy; - -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.container.jaxrs.annotation.Component; -import com.yahoo.log.LogLevel; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.Node.State; -import com.yahoo.vespa.hosted.provision.NodeRepository; -import com.yahoo.vespa.hosted.provision.node.NodeFlavors; -import com.yahoo.vespa.hosted.provision.restapi.NodeStateSerializer; -import com.yahoo.vespa.hosted.provision.restapi.legacy.ContainersForHost.DockerContainer; - -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; -import java.util.*; -import java.util.function.Predicate; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * The provisioning web service used by the provisioning controller to provide nodes to a node repository. - * - * @author mortent - */ -@Path("/provision") -@Produces(MediaType.APPLICATION_JSON) -public class ProvisionResource { - - private static final Logger log = Logger.getLogger(ProvisionResource.class.getName()); - - private final NodeRepository nodeRepository; - - private final NodeFlavors nodeFlavors; - - public ProvisionResource(@Component NodeRepository nodeRepository, @Component NodeFlavors nodeFlavors) { - super(); - this.nodeRepository = nodeRepository; - this.nodeFlavors = nodeFlavors; - } - - - @POST - @Path("/node") - @Consumes(MediaType.APPLICATION_JSON) - public void addNodes(List<HostInfo> hostInfoList) { - List<Node> nodes = new ArrayList<>(); - for (HostInfo hostInfo : hostInfoList) - nodes.add(nodeRepository.createNode(hostInfo.openStackId, hostInfo.hostname, Optional.empty(), nodeFlavors.getFlavorOrThrow(hostInfo.flavor), Node.Type.tenant)); - nodeRepository.addNodes(nodes); - } - - @GET - @Path("/node/required") - public ProvisionStatus getStatus() { - ProvisionStatus provisionStatus = new ProvisionStatus(); - provisionStatus.requiredNodes = 0; // This concept has no meaning any more ... - provisionStatus.decomissionNodes = toHostInfo(nodeRepository.getInactive()); - provisionStatus.failedNodes = toHostInfo(nodeRepository.getFailed()); - - return provisionStatus; - } - - private List<HostInfo> toHostInfo(List<Node> nodes) { - List<HostInfo> hostInfoList = new ArrayList<>(nodes.size()); - for (Node node : nodes) - hostInfoList.add(HostInfo.createHostInfo(node.hostname(), node.openStackId(), "medium")); - return hostInfoList; - } - - - @PUT - @Path("/node/ready") - public void setReady(String hostName) { - if ( nodeRepository.getNode(hostName, Node.State.ready).isPresent()) return; // node already 'ready' - - Optional<Node> node = nodeRepository.getNode(hostName, Node.State.provisioned, Node.State.dirty); - if ( ! node.isPresent()) - throw new IllegalArgumentException("Could not set " + hostName + " ready: Not registered as provisioned or dirty"); - - nodeRepository.setReady(Collections.singletonList(node.get())); - } - - @GET - @Path("/node/usage/{tenantId}") - public TenantStatus getTenantUsage(@PathParam("tenantId") String tenantId) { - TenantStatus ts = new TenantStatus(); - ts.tenantId = tenantId; - ts.allocated = nodeRepository.getNodeCount(tenantId, Node.State.active); - ts.reserved = nodeRepository.getNodeCount(tenantId, Node.State.reserved); - - Map<String, TenantStatus.ApplicationUsage> appinstanceUsageMap = new HashMap<>(); - - nodeRepository.getNodes(Node.Type.tenant, Node.State.active).stream() - .filter(node -> { - return node.allocation().get().owner().tenant().value().equals(tenantId); - }) - .forEach(node -> { - ApplicationId owner = node.allocation().get().owner(); - appinstanceUsageMap.merge( - String.format("%s:%s", owner.application().value(), owner.instance().value()), - TenantStatus.ApplicationUsage.create(owner.application().value(), owner.instance().value(), 1), - (a, b) -> { - a.usage += b.usage; - return a; - } - ); - }); - - ts.applications = new ArrayList<>(appinstanceUsageMap.values()); - return ts; - } - - //TODO: move this to nodes/v2/ when the spec for this has been nailed. - //TODO: Change it to list host nodes, instead of hosts for tenant nodes. - @GET - @Path("/dockerhost/{hostname}") - public ContainersForHost getContainersForHost(@PathParam("hostname") String hostname) { - List<DockerContainer> dockerContainersForHost = - nodeRepository.getNodes(Node.Type.tenant, State.active, State.inactive).stream() - .filter(runsOnDockerHost(hostname)) - .flatMap(ProvisionResource::toDockerContainer) - .collect(Collectors.toList()); - - return new ContainersForHost(dockerContainersForHost); - } - - //returns stream since there is no conversion from optional to stream in java. - private static Stream<DockerContainer> toDockerContainer(Node node) { - try { - String dockerImage = node.allocation().get().membership().cluster().dockerImage().orElseThrow(() -> - new Exception("Docker image not set for node " + node)); - - return Stream.of(new DockerContainer( - node.hostname(), - dockerImage, - NodeStateSerializer.wireNameOf(node.state()), - node.allocation().get().restartGeneration().wanted(), - node.allocation().get().restartGeneration().current())); - } catch (Exception e) { - log.log(LogLevel.ERROR, "Ignoring docker container.", e); - return Stream.empty(); - } - } - - private static Predicate<Node> runsOnDockerHost(String hostname) { - return node -> node.parentHostname().map(hostname::equals).orElse(false); - } -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java deleted file mode 100644 index 7e0eb41627f..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/ProvisionStatus.java +++ /dev/null @@ -1,17 +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.restapi.legacy; - -import java.util.List; - -/** - * Value class used to convert to/from JSON. - * - * @author mortent - */ -class ProvisionStatus { - - public int requiredNodes; - public List<HostInfo> decomissionNodes; - public List<HostInfo> failedNodes; - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java deleted file mode 100644 index 4f20670fa12..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/TenantStatus.java +++ /dev/null @@ -1,31 +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.restapi.legacy; - -import java.util.List; - -/** - * Value class used to convert to/from JSON. - * - * @author Oyvind Gronnesby - */ -class TenantStatus { - - public String tenantId; - public int allocated; - public int reserved; - public List<ApplicationUsage> applications; - - public static class ApplicationUsage { - public String application; - public String instance; - public int usage; - - public static ApplicationUsage create(String applicationId, String instanceId, int usage) { - ApplicationUsage appUsage = new ApplicationUsage(); - appUsage.application = applicationId; - appUsage.instance = instanceId; - appUsage.usage = usage; - return appUsage; - } - } -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java deleted file mode 100644 index 75ffa3e240e..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/legacy/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.vespa.hosted.provision.restapi.legacy; - -import com.yahoo.osgi.annotation.ExportPackage; - -/** - * Rest API which allows nodes to be added and removed from this node repository - * This API, aptly named "hack" will be removed once the dependencies are off it - Jon, March 2015 - */
\ No newline at end of file diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java index da70453c293..1df61f1c6f7 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v1/NodesApiHandler.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.restapi.v1; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.NodeType; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; @@ -22,7 +23,7 @@ import java.util.Optional; import java.util.concurrent.Executor; /** - * The implementation of the /state/v1 API. + * The implementation of the /nodes/v1 API. * This dumps the content of the node repository on request, possibly with a host filter to return just the single * matching node. * @@ -79,7 +80,7 @@ public class private void toSlime(Node.State state, Cursor object) { Cursor nodeArray = null; // create if there are nodes - for (Node.Type type : Node.Type.values()) { + for (NodeType type : NodeType.values()) { List<Node> nodes = nodeRepository.getNodes(type, state); for (Node node : nodes) { if (hostnameFilter.isPresent() && !node.hostname().equals(hostnameFilter.get())) continue; diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java index 9e240ba6055..1cea59ef79b 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesApiHandler.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.provision.restapi.v2; import com.yahoo.config.provision.HostFilter; +import com.yahoo.config.provision.NodeType; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; @@ -34,7 +35,7 @@ import java.util.logging.Level; import static com.yahoo.vespa.config.SlimeUtils.optionalString; /** - * The implementation of the /state/v2 API. + * The implementation of the /nodes/v2 API. * See RestApiTest for documentation. * * @author bratseth @@ -198,12 +199,12 @@ public class NodesApiHandler extends LoggingRequestHandler { nodeTypeFromSlime(inspector.field(nodeTypeKey))); } - private Node.Type nodeTypeFromSlime(Inspector object) { - if (! object.valid()) return Node.Type.tenant; // default + private NodeType nodeTypeFromSlime(Inspector object) { + if (! object.valid()) return NodeType.tenant; // default switch (object.asString()) { - case "tenant" : return Node.Type.tenant; - case "host" : return Node.Type.host; - case "proxy" : return Node.Type.proxy; + case "tenant" : return NodeType.tenant; + case "host" : return NodeType.host; + case "proxy" : return NodeType.proxy; default: throw new IllegalArgumentException("Unknown node type '" + object.asString() + "'"); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java index b81fe0c4417..c245230bfa3 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodesResponse.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.provision.restapi.v2; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterMembership; +import com.yahoo.config.provision.NodeType; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.slime.Cursor; @@ -102,7 +103,7 @@ class NodesResponse extends HttpResponse { /** Outputs the nodes in the given state to a node array */ private void nodesToSlime(Node.State state, Cursor parentObject) { Cursor nodeArray = parentObject.setArray("nodes"); - for (Node.Type type : Node.Type.values()) + for (NodeType type : NodeType.values()) toSlime(nodeRepository.getNodes(type, state), nodeArray); } @@ -110,7 +111,7 @@ class NodesResponse extends HttpResponse { private void nodesToSlime(Cursor parentObject) { Cursor nodeArray = parentObject.setArray("nodes"); for (Node.State state : Node.State.values()) { - for (Node.Type type : Node.Type.values()) + for (NodeType type : NodeType.values()) toSlime(nodeRepository.getNodes(type, state), nodeArray); } } @@ -175,7 +176,7 @@ class NodesResponse extends HttpResponse { toSlime(node.history(), object.setArray("history")); } - private String toString(Node.Type type) { + private String toString(NodeType type) { switch(type) { case tenant: return "tenant"; case host: return "host"; diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java index b312e7c85ca..748a5b6c558 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/FlavorConfigBuilder.java @@ -31,6 +31,19 @@ public class FlavorConfigBuilder { return flavor; } + public NodeRepositoryConfig.Flavor.Builder addNonStockFlavor(String flavorName, double cpu, double mem, double disk, Flavor.Type type) { + NodeRepositoryConfig.Flavor.Builder flavor = new NodeRepositoryConfig.Flavor.Builder(); + flavor.name(flavorName); + flavor.description("Flavor-name-is-" + flavorName); + flavor.minDiskAvailableGb(disk); + flavor.minCpuCores(cpu); + flavor.minMainMemoryAvailableGb(mem); + flavor.stock(false); + flavor.environment(type.name()); + builder.flavor(flavor); + return flavor; + } + public void addReplaces(String replaces, NodeRepositoryConfig.Flavor.Builder flavor) { NodeRepositoryConfig.Flavor.Replaces.Builder flavorReplaces = new NodeRepositoryConfig.Flavor.Replaces.Builder(); flavorReplaces.name(replaces); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java index ea6f581413e..5e1fd2357cb 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.transaction.NestedTransaction; @@ -47,22 +48,22 @@ public class MockNodeRepository extends NodeRepository { NodeRepositoryProvisioner provisioner = new NodeRepositoryProvisioner(this, flavors, Zone.defaultZone()); List<Node> nodes = new ArrayList<>(); - nodes.add(createNode("node1", "host1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant)); - nodes.add(createNode("node2", "host2.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant)); - nodes.add(createNode("node3", "host3.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("expensive"), Node.Type.tenant)); + nodes.add(createNode("node1", "host1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant)); + nodes.add(createNode("node2", "host2.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant)); + nodes.add(createNode("node3", "host3.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("expensive"), NodeType.tenant)); // TODO: Use docker flavor - Node node4 = createNode("node4", "host4.yahoo.com", Optional.of("dockerhost4"), flavors.getFlavorOrThrow("default"), Node.Type.tenant); + Node node4 = createNode("node4", "host4.yahoo.com", Optional.of("dockerhost4"), flavors.getFlavorOrThrow("default"), NodeType.tenant); node4 = node4.with(node4.status().withDockerImage("image-12")); nodes.add(node4); - Node node5 = createNode("node5", "host5.yahoo.com", Optional.of("dockerhost"), flavors.getFlavorOrThrow("default"), Node.Type.tenant); + Node node5 = createNode("node5", "host5.yahoo.com", Optional.of("dockerhost"), flavors.getFlavorOrThrow("default"), NodeType.tenant); nodes.add(node5.with(node5.status().withDockerImage("image-123").withVespaVersion(new Version("1.2.3")))); - nodes.add(createNode("node6", "host6.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant)); - nodes.add(createNode("node7", "host7.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant)); + nodes.add(createNode("node6", "host6.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant)); + nodes.add(createNode("node7", "host7.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant)); // 8 and 9 are added by web service calls - Node node10 = createNode("node10", "host10.yahoo.com", Optional.of("parent.yahoo.com"), flavors.getFlavorOrThrow("default"), Node.Type.tenant); + Node node10 = createNode("node10", "host10.yahoo.com", Optional.of("parent.yahoo.com"), flavors.getFlavorOrThrow("default"), NodeType.tenant); Status node10newStatus = node10.status(); node10newStatus = node10newStatus .withVespaVersion(Version.fromString("5.104.142")) @@ -71,8 +72,8 @@ public class MockNodeRepository extends NodeRepository { node10 = node10.with(node10newStatus); nodes.add(node10); - nodes.add(createNode("node55", "host55.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.tenant)); - nodes.add(createNode("parent1", "parent1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), Node.Type.host)); + nodes.add(createNode("node55", "host55.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant)); + nodes.add(createNode("parent1", "parent1.yahoo.com", Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.host)); nodes = addNodes(nodes); nodes.remove(6); diff --git a/node-repository/src/main/resources/configdefinitions/node-repository.def b/node-repository/src/main/resources/configdefinitions/node-repository.def index cd053adca61..f9b500594bd 100644 --- a/node-repository/src/main/resources/configdefinitions/node-repository.def +++ b/node-repository/src/main/resources/configdefinitions/node-repository.def @@ -18,6 +18,13 @@ flavor[].replaces[].name string # the expected lifetime of the node (usually three years). flavor[].cost int default=0 +# A stock flavor is any flavor which we expect to buy more of in the future. +# Stock flavors are assigned to applications by cost priority. +# +# Non-stock flavors are used for nodes for which a fixed amount has already been purchased +# for some historical reason. These nodes are assigned to applications by exact match and ignoring cost. +flavor[].stock bool default=true + # The type of node (e.g. bare metal, docker..). flavor[].environment string default="undefined" |