All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.yahoo.config.model.provision.InMemoryProvisioner Maven / Gradle / Ivy

There is a newer version: 8.409.18
Show newest version
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.model.provision;

import com.yahoo.config.provision.IntRange;
import com.yahoo.collections.ListMap;
import com.yahoo.collections.Pair;
import com.yahoo.config.model.api.HostProvisioner;
import com.yahoo.config.model.api.Provisioned;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterMembership;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.HostSpec;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.ProvisionLogger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.IntStream;

/**
 * In memory host provisioner for testing only.
 * NB! ATM cannot be reused after allocate has been called.
 *
 * @author hmusum
 * @author bratseth
 */
public class InMemoryProvisioner implements HostProvisioner {

    public static final NodeResources defaultHostResources = new NodeResources(1, 3, 50, 1);

    private final NodeResources defaultNodeResources;

    /**
     * If this is true an exception is thrown when all nodes are used.
     * If false this will simply return nodes best effort, preferring to satisfy the
     * number of groups requested when possible.
     */
    private final boolean failOnOutOfCapacity;

    /** Hosts which should be returned as retired */
    private final Set retiredHostNames;

    /** If false, nodes returned will have the resources of the host, if true node resources will be as requested */
    private final boolean sharedHosts;

    /** Free hosts of each resource size */
    private final ListMap freeNodes = new ListMap<>();
    private final Map> allocations = new LinkedHashMap<>();

    /** Indexes must be unique across all groups in a cluster */
    private final Map, Integer> nextIndexInCluster = new HashMap<>();

    /** Use this index as start index for all clusters */
    private final int startIndexForClusters;

    private final boolean useMaxResources;

    private final boolean alwaysReturnOneNode;

    private Provisioned provisioned = new Provisioned();
    private final Set clusters = new TreeSet<>(Comparator.comparing(cluster -> cluster.id().value()));

    private Environment environment = Environment.prod;

    /** Creates this with a number of nodes with resources 1, 3, 9, 1 */
    public InMemoryProvisioner(int nodeCount, boolean sharedHosts) {
        this(nodeCount, defaultHostResources, sharedHosts);
    }

    /** Creates this with a number of nodes with given resources */
    public InMemoryProvisioner(int nodeCount, NodeResources resources, boolean sharedHosts) {
        this(Map.of(resources, createHostInstances(nodeCount)), true, false, false, sharedHosts, NodeResources.unspecified(), 0);
    }

    /** Creates this with a number of nodes with given resources */
    public InMemoryProvisioner(int nodeCount, NodeResources resources, boolean sharedHosts, NodeResources defaultResources) {
        this(Map.of(resources, createHostInstances(nodeCount)), true, false, false, sharedHosts, defaultResources, 0);
    }

    /** Creates this with a set of host names of the flavor 'default' */
    public InMemoryProvisioner(boolean failOnOutOfCapacity, boolean sharedHosts, String... hosts) {
        this(Map.of(defaultHostResources, toHostInstances(hosts)), failOnOutOfCapacity, false, false, sharedHosts, defaultHostResources, 0);
    }

    /** Creates this with a set of host names of the flavor 'default' */
    public InMemoryProvisioner(boolean failOnOutOfCapacity, boolean sharedHosts, List hosts) {
        this(Map.of(defaultHostResources, toHostInstances(hosts.toArray(new String[0]))), failOnOutOfCapacity, false, false, sharedHosts, defaultHostResources, 0);
    }

    /** Creates this with a set of hosts of the flavor 'default' */
    public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, boolean sharedHosts, String ... retiredHostNames) {
        this(Map.of(defaultHostResources, hosts.asCollection()), failOnOutOfCapacity, false, false, sharedHosts, defaultHostResources, 0, retiredHostNames);
    }

    /** Creates this with a set of hosts of the flavor 'default' */
    public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, boolean sharedHosts, int startIndexForClusters, String ... retiredHostNames) {
        this(Map.of(defaultHostResources, hosts.asCollection()), failOnOutOfCapacity, false, false, sharedHosts, defaultHostResources, startIndexForClusters, retiredHostNames);
    }

    public InMemoryProvisioner(Map> hosts,
                               boolean failOnOutOfCapacity,
                               boolean useMaxResources,
                               boolean alwaysReturnOneNode,
                               boolean sharedHosts,
                               NodeResources defaultResources,
                               int startIndexForClusters,
                               String ... retiredHostNames) {
        this.defaultNodeResources = defaultResources;
        this.failOnOutOfCapacity = failOnOutOfCapacity;
        this.useMaxResources = useMaxResources;
        this.alwaysReturnOneNode = alwaysReturnOneNode;
        for (Map.Entry> hostsWithResources : hosts.entrySet())
            for (Host host : hostsWithResources.getValue())
                freeNodes.put(hostsWithResources.getKey(), host);
        this.sharedHosts = sharedHosts;
        this.startIndexForClusters = startIndexForClusters;
        this.retiredHostNames = Set.of(retiredHostNames);
    }

    public Provisioned provisioned() { return provisioned; }

    /** May affect e.g. the number of nodes/cluster. */
    public InMemoryProvisioner setEnvironment(Environment environment) {
        this.environment = environment;
        return this;
    }

    private static Collection toHostInstances(String[] hostnames) {
        return Arrays.stream(hostnames).map(Host::new).toList();
    }

    private static Collection createHostInstances(int hostCount) {
        return IntStream.range(1, hostCount + 1).mapToObj(i -> new Host("host" + i)).toList();
    }

    /** Returns the current allocations of this as a mutable map */
    public Map> allocations() { return allocations; }

    @Override
    public HostSpec allocateHost(String alias) {
        List defaultHosts = freeNodes.get(defaultHostResources);
        if (defaultHosts.isEmpty()) throw new IllegalArgumentException("No more hosts with default resources available");
        Host newHost = freeNodes.removeValue(defaultHostResources, 0);
        return new HostSpec(newHost.hostname(), Optional.empty());
    }

    @Override
    public List prepare(ClusterSpec cluster, Capacity requested, ProvisionLogger logger) {
        provisioned.add(cluster, requested);
        clusters.add(cluster);
        if (environment == Environment.dev && ! requested.isRequired()) {
            requested = requested.withLimits(requested.minResources().withNodes(1),
                                             requested.maxResources().withNodes(1));
        }
        IntRange groupRange = IntRange.of(requested.minResources().groups(), requested.maxResources().groups());
        if (useMaxResources) {
            int groups = groupRange.fit(requested.maxResources().nodes() / requested.groupSize().to().orElse(1));
            return prepare(cluster, requested.maxResources(),groups, requested.isRequired(), requested.canFail());
        }
        else {
            int groups = groupRange.fit(requested.minResources().nodes() / requested.groupSize().from().orElse(1));
            return prepare(cluster, requested.minResources(), groups, requested.isRequired(), requested.canFail());
        }
    }

    public List prepare(ClusterSpec cluster, ClusterResources requested, int groups, boolean required, boolean canFail) {
        if (cluster.group().isPresent() && requested.groups() > 1)
            throw new IllegalArgumentException("Cannot both be specifying a group and ask for groups to be created");

        int nodes = failOnOutOfCapacity || required
                       ? requested.nodes()
                       : Math.min(requested.nodes(), freeNodes.get(defaultHostResources).size() + totalAllocatedTo(cluster));
        if (alwaysReturnOneNode)
            nodes = 1;

        groups = Math.min(groups, nodes);

        List allocation = new ArrayList<>();
        if (groups == 1) {
            allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(0))),
                                                requested.nodeResources(),
                                                nodes,
                                                startIndexForClusters,
                                                canFail));
        }
        else {
            for (int i = 0; i < groups; i++) {
                allocation.addAll(allocateHostGroup(cluster.with(Optional.of(ClusterSpec.Group.from(i))),
                                                    requested.nodeResources(),
                                                    nodes / groups,
                                                    allocation.size(),
                                                    canFail));
            }
        }
        for (ListIterator i = allocation.listIterator(); i.hasNext(); ) {
            HostSpec host = i.next();
            if (retiredHostNames.contains(host.hostname()))
                i.set(retire(host));
        }

        return allocation;
    }

    /** Create a new provisioned instance to record provision requests to this and returns it */
    public Provisioned startProvisionedRecording() {
        provisioned = new Provisioned();
        clusters.clear();
        return provisioned;
    }

    private HostSpec retire(HostSpec host) {
        return new HostSpec(host.hostname(),
                            host.realResources(),
                            host.advertisedResources(),
                            host.requestedResources().orElse(NodeResources.unspecified()),
                            host.membership().get().retire(),
                            host.version(),
                            Optional.empty(),
                            host.dockerImageRepo());
    }

    // Minimal capacity policies
    private NodeResources decideResources(NodeResources resources) {
        if (defaultNodeResources.isUnspecified()) return resources;
        return resources.withUnspecifiedFieldsFrom(defaultNodeResources);
    }

    private List allocateHostGroup(ClusterSpec clusterGroup, NodeResources requestedResourcesOrUnspecified,
                                             int nodesInGroup, int startIndex, boolean canFail) {
        var requestedResources = decideResources(requestedResourcesOrUnspecified);
        List allocation = allocations.getOrDefault(clusterGroup, new ArrayList<>());
        allocations.put(clusterGroup, allocation);

        // Check if the current allocations are compatible with the new request
        for (int i = allocation.size() - 1; i >= 0; i--) {
            NodeResources currentResources = allocation.get(0).advertisedResources();
            if (currentResources.isUnspecified() || requestedResources.isUnspecified()) continue;
            if ( (! sharedHosts && ! currentResources.satisfies(requestedResources))
                 ||
                 (sharedHosts && ! currentResources.compatibleWith(requestedResources))) {
                HostSpec removed = allocation.remove(i);
                freeNodes.put(currentResources, new Host(removed.hostname())); // Return the node back to free pool
            }
        }

        int nextIndex = nextIndexInCluster.getOrDefault(new Pair<>(clusterGroup.type(), clusterGroup.id()), startIndex);
        while (nonRetiredIn(allocation).size() < nodesInGroup) {
            // Find the smallest host that can fit the requested resources
            Optional hostResources = freeNodes.keySet().stream()
                    .sorted(new MemoryDiskCpu())
                    .filter(resources -> requestedResources.isUnspecified() || resources.satisfies(requestedResources))
                    .findFirst();
            if (hostResources.isEmpty()) {
                if (canFail)
                    throw new IllegalArgumentException("Insufficient capacity for " + requestedResources + " in cluster " + clusterGroup);
                else
                    break; // ¯\_(ツ)_/¯
            }

            Host newHost = freeNodes.removeValue(hostResources.get(), 0);
            if (freeNodes.get(hostResources.get()).isEmpty()) freeNodes.removeAll(hostResources.get());
            ClusterMembership membership = ClusterMembership.from(clusterGroup, nextIndex++);
            NodeResources resources = sharedHosts ? requestedResources : hostResources.get();
            allocation.add(new HostSpec(newHost.hostname(),
                                        resources, resources, requestedResources,
                                        membership,
                                        newHost.version(), Optional.empty(),
                                        Optional.empty()));
        }
        nextIndexInCluster.put(new Pair<>(clusterGroup.type(), clusterGroup.id()), nextIndex);

        while (nonRetiredIn(allocation).size() > nodesInGroup)
            allocation.remove(0);

        return allocation;
    }

    private List nonRetiredIn(List hosts) {
        return hosts.stream().filter(host -> ! retiredHostNames.contains(host.hostname())).toList();
    }

    private int totalAllocatedTo(ClusterSpec cluster) {
        int count = 0;
        for (Map.Entry> allocation : allocations.entrySet()) {
            if ( ! allocation.getKey().type().equals(cluster.type())) continue;
            if ( ! allocation.getKey().id().equals(cluster.id())) continue;
            count += allocation.getValue().size();
        }
        return count;
    }

    private static class MemoryDiskCpu implements Comparator {

        @Override
        public int compare(NodeResources a, NodeResources b) {
            if (a.memoryGiB() > b.memoryGiB()) return 1;
            if (a.memoryGiB() < b.memoryGiB()) return -1;
            if (a.diskGb() > b.diskGb()) return 1;
            if (a.diskGb() < b.diskGb()) return -1;
            return Double.compare(a.vcpu(), b.vcpu());
        }
    }

    public Set provisionedClusters() { return clusters; }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy