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

com.yahoo.vespa.hosted.provision.applications.Cluster Maven / Gradle / Ivy

There is a newer version: 8.441.21
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.vespa.hosted.provision.applications;

import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterInfo;
import com.yahoo.config.provision.IntRange;
import com.yahoo.config.provision.Capacity;
import com.yahoo.config.provision.ClusterResources;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.vespa.hosted.provision.autoscale.Autoscaler;
import com.yahoo.vespa.hosted.provision.autoscale.Autoscaling;
import com.yahoo.vespa.hosted.provision.autoscale.ClusterModel;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * The node repo's view of a cluster in an application deployment.
 *
 * This is immutable, and must be locked with the application lock on read-modify-write.
 *
 * @author bratseth
 */
public class Cluster {

    public static final int maxScalingEvents = 15;

    private final ClusterSpec.Id id;
    private final boolean exclusive;
    private final ClusterResources min, max;
    private final IntRange groupSize;
    private final boolean required;
    private final Optional cloudAccount;
    private final List suggestions;
    private final Autoscaling target;
    private final ClusterInfo clusterInfo;
    private final BcpGroupInfo bcpGroupInfo;

    /** The maxScalingEvents last scaling events of this, sorted by increasing time (newest last) */
    private final List scalingEvents;

    public Cluster(ClusterSpec.Id id,
                   boolean exclusive,
                   ClusterResources minResources,
                   ClusterResources maxResources,
                   IntRange groupSize,
                   boolean required,
                   Optional cloudAccount,
                   List suggestions,
                   Autoscaling target,
                   ClusterInfo clusterInfo,
                   BcpGroupInfo bcpGroupInfo,
                   List scalingEvents) {
        this.id = Objects.requireNonNull(id);
        this.exclusive = exclusive;
        this.min = Objects.requireNonNull(minResources);
        this.max = Objects.requireNonNull(maxResources);
        this.groupSize = Objects.requireNonNull(groupSize);
        this.required = required;
        this.cloudAccount = Objects.requireNonNull(cloudAccount);
        this.suggestions = Objects.requireNonNull(suggestions);
        Objects.requireNonNull(target);
        if (target.resources().isPresent() && ! target.resources().get().isWithin(minResources, maxResources))
            this.target = target.withResources(Optional.empty()); // Delete illegal target
        else
            this.target = target;
        this.clusterInfo = clusterInfo;
        this.bcpGroupInfo = Objects.requireNonNull(bcpGroupInfo);
        this.scalingEvents = List.copyOf(scalingEvents);
    }

    public ClusterSpec.Id id() { return id; }

    /** Returns whether the nodes allocated to this cluster must be on host exclusively dedicated to this application */
    public boolean exclusive() { return exclusive; }

    /** Returns the configured minimal resources in this cluster */
    public ClusterResources minResources() { return min; }

    /** Returns the configured maximal resources in this cluster */
    public ClusterResources maxResources() { return max; }

    /** Returns the configured group size range in this cluster */
    public IntRange groupSize() { return groupSize; }

    /**
     * Returns whether the resources of this cluster are required to be within the specified min and max.
     * Otherwise, they may be adjusted by capacity policies.
     */
    public boolean required() { return required; }

    /** Returns the enclave cloud account of this cluster, or empty if not enclave. */
    public Optional cloudAccount() { return cloudAccount; }

    /**
     * Returns the computed resources (between min and max, inclusive) this cluster should
     * have allocated at the moment (whether or not it actually has it),
     * or empty if the system currently has no target.
     */
    public Autoscaling target() { return target; }

    /**
     * The list of suggested resources, which may or may not be within the min and max limits,
     * or empty if there is currently no recorded suggestion.
     * List is sorted by preference
     */
    public List suggestions() { return suggestions; }

    /** Returns true if there is a current suggestion and we should actually make this suggestion to users. */
    public boolean shouldSuggestResources(ClusterResources currentResources) {
        if (suggestions.isEmpty()) return false;
        return suggestions.stream().noneMatch(suggestion ->
                        suggestion.resources().isEmpty()
                                || suggestion.resources().get().isWithin(min, max)
                                || ! Autoscaler.worthRescaling(currentResources, suggestion.resources().get())
        );
    }

    public ClusterInfo clusterInfo() { return clusterInfo; }

    /** Returns info about the BCP group of clusters this belongs to. */
    public BcpGroupInfo bcpGroupInfo() { return bcpGroupInfo; }

    /** Returns the recent scaling events in this cluster */
    public List scalingEvents() { return scalingEvents; }

    public Optional lastScalingEvent() {
        if (scalingEvents.isEmpty()) return Optional.empty();
        return Optional.of(scalingEvents.get(scalingEvents.size() - 1));
    }

    /** Returns whether the last scaling event in this has yet to complete. */
    public boolean scalingInProgress() {
        return lastScalingEvent().isPresent() && lastScalingEvent().get().completion().isEmpty();
    }

    public Cluster withConfiguration(boolean exclusive, Capacity capacity) {
        return new Cluster(id, exclusive,
                           capacity.minResources(), capacity.maxResources(), capacity.groupSize(), capacity.isRequired(),
                           capacity.cloudAccount(), suggestions, target, capacity.clusterInfo(), bcpGroupInfo, scalingEvents);
    }

    public Cluster withSuggestions(List suggestions) {
        return new Cluster(id, exclusive, min, max, groupSize, required, cloudAccount, suggestions, target, clusterInfo, bcpGroupInfo, scalingEvents);
    }

    public Cluster withTarget(Autoscaling target) {
        return new Cluster(id, exclusive, min, max, groupSize, required, cloudAccount, suggestions, target, clusterInfo, bcpGroupInfo, scalingEvents);
    }

    public Cluster with(BcpGroupInfo bcpGroupInfo) {
        return new Cluster(id, exclusive, min, max, groupSize, required, cloudAccount, suggestions, target, clusterInfo, bcpGroupInfo, scalingEvents);
    }

    /** Add or update (based on "at" time) a scaling event */
    public Cluster with(ScalingEvent scalingEvent) {
        List scalingEvents = new ArrayList<>(this.scalingEvents);

        int existingIndex = eventIndexAt(scalingEvent.at());
        if (existingIndex >= 0)
            scalingEvents.set(existingIndex, scalingEvent);
        else
            scalingEvents.add(scalingEvent);

        prune(scalingEvents);
        return new Cluster(id, exclusive, min, max, groupSize, required, cloudAccount, suggestions, target, clusterInfo, bcpGroupInfo, scalingEvents);
    }

    @Override
    public int hashCode() { return id.hashCode(); }

    @Override
    public boolean equals(Object other) {
        if (other == this) return true;
        if ( ! (other instanceof Cluster)) return false;
        return ((Cluster)other).id().equals(this.id);
    }

    @Override
    public String toString() { return id.toString(); }

    private void prune(List scalingEvents) {
        while (scalingEvents.size() > maxScalingEvents)
            scalingEvents.remove(0);
    }

    private int eventIndexAt(Instant at) {
        for (int i = 0; i < scalingEvents.size(); i++) {
            if (scalingEvents.get(i).at().equals(at))
                return i;
        }
        return -1;
    }

    public static Cluster create(ClusterSpec.Id id, boolean exclusive, Capacity requested) {
        return new Cluster(id, exclusive,
                           requested.minResources(), requested.maxResources(), requested.groupSize(), requested.isRequired(),
                           requested.cloudAccount(), List.of(), Autoscaling.empty(), requested.clusterInfo(), BcpGroupInfo.empty(), List.of());
    }

    /** The predicted time it will take to rescale this cluster. */
    public Duration scalingDuration() {
        int completedEventCount = 0;
        Duration totalDuration = Duration.ZERO;
        for (ScalingEvent event : scalingEvents()) {
            if (event.duration().isEmpty()) continue;
            // Assume we have missed timely recording completion if it is longer than 4 days, so ignore
            if ( ! event.duration().get().minus(Duration.ofDays(4)).isNegative()) continue;

            completedEventCount++;
            totalDuration = totalDuration.plus(event.duration().get());
        }
        if (completedEventCount == 0) return ClusterModel.minScalingDuration();
        return minimum(ClusterModel.minScalingDuration(), totalDuration.dividedBy(completedEventCount));
    }

    /** The predicted time this cluster will stay in each resource configuration (including the scaling duration). */
    public Duration allocationDuration(ClusterSpec clusterSpec) {
        if (scalingEvents.size() < 2) return Duration.ofHours(12); // Default

        long totalDurationMs = 0;
        for (int i = 1; i < scalingEvents().size(); i++)
            totalDurationMs += scalingEvents().get(i).at().toEpochMilli() - scalingEvents().get(i - 1).at().toEpochMilli();
        return Duration.ofMillis(totalDurationMs / (scalingEvents.size() - 1));
    }

    private static Duration minimum(Duration smallestAllowed, Duration duration) {
        if (duration.minus(smallestAllowed).isNegative())
            return smallestAllowed;
        return duration;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy