com.yahoo.vespa.hosted.provision.applications.Cluster Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of node-repository Show documentation
Show all versions of node-repository Show documentation
Keeps track of node assignment in a multi-application setup.
The 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;
}
}