/** Returns true if this filter matches the given host properties */ public boolean matches(String hostname, String flavor, Optional<ClusterMembership> membership) { if ( ! hostnames.isEmpty() && ! hostnames.contains(hostname)) return false; if ( ! flavors.isEmpty() && ! flavors.contains(flavor)) return false; if ( ! clusterTypes.isEmpty() && ! (membership.isPresent() && clusterTypes.contains(membership.get().cluster().type()))) return false; if ( ! clusterIds.isEmpty() && ! (membership.isPresent() && clusterIds.contains(membership.get().cluster().id()))) return false; return true; }
/** Collect hosts per group */ private Map<Optional<ClusterSpec.Group>, Map<HostResource, ClusterMembership>> collectAllocatedSubgroups(Map<HostResource, ClusterMembership> hostMapping) { Map<Optional<ClusterSpec.Group>, Map<HostResource, ClusterMembership>> hostsPerGroup = new LinkedHashMap<>(); for (Map.Entry<HostResource, ClusterMembership> entry : hostMapping.entrySet()) { Optional<ClusterSpec.Group> group = entry.getValue().cluster().group(); Map<HostResource, ClusterMembership> hostsInGroup = hostsPerGroup.get(group); if (hostsInGroup == null) { hostsInGroup = new LinkedHashMap<>(); hostsPerGroup.put(group, hostsInGroup); } hostsInGroup.put(entry.getKey(), entry.getValue()); } return hostsPerGroup; }
private void validate(Collection<HostSpec> hosts) { for (HostSpec host : hosts) { if ( ! host.membership().isPresent()) throw new IllegalArgumentException("Hosts must be assigned a cluster when activating, but got " + host); if ( ! host.membership().get().cluster().group().isPresent()) throw new IllegalArgumentException("Hosts must be assigned a group when activating, but got " + host); } }
@Override protected void maintain() { List<Node> containerNodes = getExpiredNodes(containerExpiry) .stream() .filter(node -> node.allocation().isPresent() && node.allocation().get().membership().cluster().type() == ClusterSpec.Type.container) .collect(Collectors.toList()); List<Node> remainingNodes = getExpiredNodes(defaultExpiry); remainingNodes.removeAll(containerNodes); recycle(containerNodes); recycle(remainingNodes); }
/** Move nodes from unwanted groups to wanted groups to avoid lingering groups consisting of retired nodes */ private void moveToActiveGroup(List<Node> surplusNodes, int wantedGroups, Optional<ClusterSpec.Group> targetGroup) { for (ListIterator<Node> i = surplusNodes.listIterator(); i.hasNext(); ) { Node node = i.next(); ClusterMembership membership = node.allocation().get().membership(); ClusterSpec cluster = membership.cluster(); if (cluster.group().get().index() >= wantedGroups) { ClusterSpec.Group newGroup = targetGroup.orElse(ClusterSpec.Group.from(0)); ClusterMembership newGroupMembership = membership.with(cluster.with(Optional.of(newGroup))); i.set(node.with(node.allocation().get().with(newGroupMembership))); } } }
/** Returns a list of active containers for given application, grouped by cluster spec */ private Map<ClusterSpec, List<Node>> activeContainers(ApplicationId application) { return new NodeList(nodeRepository.getNodes(Node.State.active)) .owner(application) .type(ClusterSpec.Type.container) .asList() .stream() .collect(Collectors.groupingBy(n -> n.allocation().get().membership().cluster())); }
private void toSlime(ClusterMembership membership, Cursor object) { object.setString("clustertype", membership.cluster().type().name()); object.setString("clusterid", membership.cluster().id().value()); object.setString("group", String.valueOf(membership.cluster().group().get().index())); object.setLong("index", membership.index()); object.setBool("retired", membership.retired()); }
/** * If a parent host is given, and it hosts another tenant with an application which requires exclusive access * to the physical host, then we cannot host this application on it. */ private boolean exclusiveTo(TenantName tenant, Optional<String> parentHostname) { if ( ! parentHostname.isPresent()) return true; for (Node nodeOnHost : nodeRepository.list().childrenOf(parentHostname.get())) { if ( ! nodeOnHost.allocation().isPresent()) continue; if ( nodeOnHost.allocation().get().membership().cluster().isExclusive() && ! nodeOnHost.allocation().get().owner().tenant().equals(tenant)) return false; } return true; }
/** Returns the subset of nodes assigned to the given cluster type */ public NodeList type(ClusterSpec.Type type) { return filter(node -> node.allocation().get().membership().cluster().type().equals(type)); }
private Node move(Node node, Node.State toState, Agent agent, Optional<String> reason) { if (toState == Node.State.active && ! node.allocation().isPresent()) throw new IllegalArgumentException("Could not set " + node.hostname() + " active. It has no allocation."); try (Mutex lock = lock(node)) { if (toState == Node.State.active) { for (Node currentActive : getNodes(node.allocation().get().owner(), Node.State.active)) { if (node.allocation().get().membership().cluster().equals(currentActive.allocation().get().membership().cluster()) && node.allocation().get().membership().index() == currentActive.allocation().get().membership().index()) throw new IllegalArgumentException("Could not move " + node + " to active:" + "It has the same cluster and index as an existing node"); } } return db.writeTo(toState, node, agent, reason); } }
NodePrioritizer(List<Node> allNodes, ApplicationId appId, ClusterSpec clusterSpec, NodeSpec nodeSpec, int spares, NameResolver nameResolver) { this.allNodes = Collections.unmodifiableList(allNodes); this.requestedNodes = nodeSpec; this.clusterSpec = clusterSpec; this.appId = appId; this.nameResolver = nameResolver; this.spareHosts = findSpareHosts(allNodes, spares); this.capacity = new DockerHostCapacity(allNodes); long nofFailedNodes = allNodes.stream() .filter(node -> node.state().equals(Node.State.failed)) .filter(node -> node.allocation().isPresent()) .filter(node -> node.allocation().get().owner().equals(appId)) .filter(node -> node.allocation().get().membership().cluster().id().equals(clusterSpec.id())) .count(); long nofNodesInCluster = allNodes.stream() .filter(node -> node.allocation().isPresent()) .filter(node -> node.allocation().get().owner().equals(appId)) .filter(node -> node.allocation().get().membership().cluster().id().equals(clusterSpec.id())) .count(); this.isAllocatingForReplacement = isReplacement(nofNodesInCluster, nofFailedNodes); this.isDocker = isDocker(); }
private static int compareForRelocation(Node a, Node b) { // Choose smallest node int capacity = ResourceCapacity.of(a).compare(ResourceCapacity.of(b)); if (capacity != 0) return capacity; // Choose unallocated over allocated (this case is when we have ready docker nodes) if (!a.allocation().isPresent() && b.allocation().isPresent()) return -1; if (a.allocation().isPresent() && !b.allocation().isPresent()) return 1; // Choose container over content nodes if (a.allocation().isPresent() && b.allocation().isPresent()) { if (a.allocation().get().membership().cluster().type().equals(ClusterSpec.Type.container) && !b.allocation().get().membership().cluster().type().equals(ClusterSpec.Type.container)) return -1; if (!a.allocation().get().membership().cluster().type().equals(ClusterSpec.Type.container) && b.allocation().get().membership().cluster().type().equals(ClusterSpec.Type.container)) return 1; } // To get a stable algorithm - choose lexicographical from hostname return a.hostname().compareTo(b.hostname()); }
/** * Returns a list of the nodes which are * in groups with index number above or equal the group count */ private List<Node> findNodesInRemovableGroups(ApplicationId application, ClusterSpec requestedCluster, int wantedGroups) { List<Node> surplusNodes = new ArrayList<>(0); for (Node node : nodeRepository.getNodes(application, Node.State.active)) { ClusterSpec nodeCluster = node.allocation().get().membership().cluster(); if ( ! nodeCluster.id().equals(requestedCluster.id())) continue; if ( ! nodeCluster.type().equals(requestedCluster.type())) continue; if (nodeCluster.group().get().index() >= wantedGroups) surplusNodes.add(node); } return surplusNodes; }
/** * Returns whether this node should be accepted into the cluster even if it is not currently desired * (already enough nodes, or wrong flavor). * Such nodes will be marked retired during finalization of the list of accepted nodes. * The conditions for this are * <ul> * <li>This is a content node. These must always be retired before being removed to allow the cluster to * migrate away data. * <li>This is a container node and it is not desired due to having the wrong flavor. In this case this * will (normally) obtain for all the current nodes in the cluster and so retiring before removing must * be used to avoid removing all the current nodes at once, before the newly allocated replacements are * initialized. (In the other case, where a container node is not desired because we have enough nodes we * do want to remove it immediately to get immediate feedback on how the size reduction works out.) * </ul> */ private boolean acceptToRetire(Node node) { if (node.state() != Node.State.active) return false; if (! node.allocation().get().membership().cluster().group().equals(cluster.group())) return false; return (cluster.type() == ClusterSpec.Type.content) || (cluster.type() == ClusterSpec.Type.container && ! hasCompatibleFlavor(node)); }
private void toSlime(HostSpec host, Cursor cursor) { cursor.setString(hostSpecHostName, host.hostname()); host.membership().ifPresent(membership -> { cursor.setString(hostSpecMembership, membership.stringValue()); cursor.setString(hostSpecVespaVersion, membership.cluster().vespaVersion().toFullString()); }); host.flavor().ifPresent(flavor -> cursor.setString(hostSpecFlavor, flavor.name())); host.version().ifPresent(version -> cursor.setString(hostSpecCurrentVespaVersion, version.toFullString())); }
private String getHostFromVespaCertificate(List<SubjectAlternativeName> sans) { // TODO Remove this branch once all BM nodes are gone if (sans.stream().anyMatch(san -> san.getValue().endsWith("ostk.yahoo.cloud"))) { return getHostFromCalypsoCertificate(sans); } VespaUniqueInstanceId instanceId = VespaUniqueInstanceId.fromDottedString(getUniqueInstanceId(sans)); if (!zone.environment().value().equals(instanceId.environment())) throw new NodeIdentifierException("Invalid environment: " + instanceId.environment()); if (!zone.region().value().equals(instanceId.region())) throw new NodeIdentifierException("Invalid region(): " + instanceId.region()); List<Node> applicationNodes = nodeRepository.getNodes(ApplicationId.from(instanceId.tenant(), instanceId.application(), instanceId.instance())); return applicationNodes.stream() .filter( node -> node.allocation() .map(allocation -> allocation.membership().index() == instanceId.clusterIndex() && allocation.membership().cluster().id().value().equals(instanceId.clusterId())) .orElse(false)) .map(Node::hostname) .findFirst() .orElseThrow(() -> new NodeIdentifierException("Could not find any node with instance id: " + instanceId.asDottedString())); }
/** * Returns the highest index number of all active and failed nodes in this cluster, or -1 if there are no nodes. * We include failed nodes to avoid reusing the index of the failed node in the case where the failed node is the * node with the highest index. */ private int findHighestIndex(ApplicationId application, ClusterSpec cluster) { int highestIndex = -1; for (Node node : nodeRepository.getNodes(application, Node.State.active, Node.State.inactive, Node.State.parked, Node.State.failed)) { ClusterSpec nodeCluster = node.allocation().get().membership().cluster(); if ( ! nodeCluster.id().equals(cluster.id())) continue; if ( ! nodeCluster.type().equals(cluster.type())) continue; highestIndex = Math.max(node.allocation().get().membership().index(), highestIndex); } return highestIndex; }
private Node acceptNode(PrioritizableNode prioritizableNode, boolean wantToRetire) { Node node = prioritizableNode.node; if (! wantToRetire) { if ( ! node.state().equals(Node.State.active)) { // reactivated node - make sure its not retired node = node.unretire(); prioritizableNode.node = node; } acceptedOfRequestedFlavor++; } else { ++wasRetiredJustNow; // Retire nodes which are of an unwanted flavor, retired flavor or have an overlapping parent host node = node.retire(nodeRepository.clock().instant()); prioritizableNode.node = node; } if ( ! node.allocation().get().membership().cluster().equals(cluster)) { // group may be different node = setCluster(cluster, node); prioritizableNode.node = node; } indexes.add(node.allocation().get().membership().index()); highestIndex.set(Math.max(highestIndex.get(), node.allocation().get().membership().index())); nodes.add(prioritizableNode); return node; }
private void toSlime(Allocation allocation, Cursor object) { object.setString(tenantIdKey, allocation.owner().tenant().value()); object.setString(applicationIdKey, allocation.owner().application().value()); object.setString(instanceIdKey, allocation.owner().instance().value()); object.setString(serviceIdKey, allocation.membership().stringValue()); object.setLong(restartGenerationKey, allocation.restartGeneration().wanted()); object.setLong(currentRestartGenerationKey, allocation.restartGeneration().current()); object.setBool(removableKey, allocation.isRemovable()); object.setString(wantedVespaVersionKey, allocation.membership().cluster().vespaVersion().toString()); }