// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model.application.validation.change;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.model.provision.InMemoryProvisioner;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.NodeResources;
import com.yahoo.vespa.model.VespaModel;
import com.yahoo.vespa.model.application.validation.ValidationTester;
import com.yahoo.yolean.Exceptions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/**
* @author freva
* @author bratseth
*/
public class ResourcesReductionValidatorTest {
private static final String CONTAINER_CLUSTER = "default.indexing";
private final NodeResources hostResources = new NodeResources(64, 128, 1000, 10);
private final InMemoryProvisioner provisioner = new InMemoryProvisioner(30, hostResources, true, InMemoryProvisioner.defaultHostResources);
private final InMemoryProvisioner provisionerSelfHosted = new InMemoryProvisioner(30, hostResources, true, NodeResources.unspecified());
private final NodeResources defaultResources = InMemoryProvisioner.defaultHostResources;
private final ValidationTester tester = new ValidationTester(provisioner);
@Test
void fail_when_reduction_by_over_50_percent() {
var fromResources = new NodeResources(8, 64, 800, 1);
var toResources = new NodeResources(8, 16, 800, 1);
VespaModel previous = tester.deploy(null, contentServices(6, fromResources), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
try {
tester.deploy(previous, contentServices(6, toResources), Environment.prod, null, CONTAINER_CLUSTER);
fail("Expected exception due to resources reduction");
} catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
fromResources.multipliedBy(6),
toResources.multipliedBy(6));
}
}
@Test
void fail_when_reducing_multiple_resources_by_over_50_percent() {
var fromResources = new NodeResources(8, 64, 800, 1);
var toResources = new NodeResources(3, 16, 200, 1);
VespaModel previous = tester.deploy(null, contentServices(6, fromResources), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
try {
tester.deploy(previous, contentServices(6, toResources), Environment.prod, null, CONTAINER_CLUSTER);
fail("Expected exception due to resources reduction");
} catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
fromResources.multipliedBy(6),
toResources.multipliedBy(6));
}
}
@Test
void small_resource_decrease_is_allowed() {
VespaModel previous = tester.deploy(null, contentServices(6, new NodeResources(1.5, 64, 800, 1)), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(6, new NodeResources(.5, 48, 600, 1)), Environment.prod, null, CONTAINER_CLUSTER);
}
@Test
void reorganizing_resources_is_allowed() {
VespaModel previous = tester.deploy(null, contentServices(12, new NodeResources(2, 10, 100, 1)), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(4, new NodeResources(6, 30, 300, 1)), Environment.prod, null, CONTAINER_CLUSTER);
}
@Test
void overriding_resource_decrease() {
VespaModel previous = tester.deploy(null, contentServices(6, new NodeResources(8, 64, 800, 1)), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(6, new NodeResources(8, 16, 800, 1)), Environment.prod, resourcesReductionOverride, CONTAINER_CLUSTER); // Allowed due to override
}
@Test
void reduction_is_detected_when_going_from_unspecified_resources_container() {
NodeResources toResources = defaultResources.withDiskGb(defaultResources.diskGb() / 5);
try {
VespaModel previous = tester.deploy(null, containerServices(6, null), Environment.prod, null).getFirst();
tester.deploy(previous, containerServices(6, toResources), Environment.prod, null);
fail("Expected exception due to resources reduction");
}
catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
defaultResources.multipliedBy(6),
toResources.multipliedBy(6));
}
}
@Test
void reduction_is_detected_when_going_to_unspecified_resources_container() {
NodeResources fromResources = defaultResources.withVcpu(defaultResources.vcpu() * 3);
try {
VespaModel previous = tester.deploy(null, containerServices(6, fromResources), Environment.prod, null).getFirst();
tester.deploy(previous, containerServices(6, null), Environment.prod, null);
fail("Expected exception due to resources reduction");
}
catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
fromResources.multipliedBy(6),
defaultResources.multipliedBy(6));
}
}
@Test
void reduction_is_detected_when_going_from_unspecified_resources_content() {
NodeResources toResources = defaultResources.withDiskGb(defaultResources.diskGb() / 5);
try {
VespaModel previous = tester.deploy(null, contentServices(6, null), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(6, toResources), Environment.prod, null, CONTAINER_CLUSTER);
fail("Expected exception due to resources reduction");
} catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
defaultResources.multipliedBy(6),
toResources.multipliedBy(6));
}
}
@Test
void reduction_is_detected_when_going_to_unspecified_resources_content() {
NodeResources fromResources = defaultResources.withVcpu(defaultResources.vcpu() * 3);
try {
VespaModel previous = tester.deploy(null, contentServices(6, fromResources), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(6, null), Environment.prod, null, CONTAINER_CLUSTER);
fail("Expected exception due to resources reduction");
}
catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
fromResources.multipliedBy(6),
defaultResources.multipliedBy(6));
}
}
@Test
void testSizeReductionValidationWithUnspecifiedResourcesHosted() {
int fromNodes = 30;
int toNodes = 14;
try {
ValidationTester tester = new ValidationTester(33);
VespaModel previous = tester.deploy(null, contentServices(fromNodes, null), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(toNodes, null), Environment.prod, null, CONTAINER_CLUSTER);
fail("Expected exception due to resources reduction");
}
catch (IllegalArgumentException expected) {
assertResourceReductionException(expected,
defaultResources.multipliedBy(fromNodes),
defaultResources.multipliedBy(toNodes));
}
}
/** Emulate a self-hosted setup in only the sense that it does not set node resources on the provisioned hosts. */
@Test
void testSizeReductionValidationSelfhosted() {
var tester = new ValidationTester(provisionerSelfHosted);
VespaModel previous = tester.deploy(null, contentServices(10, null), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
try {
tester.deploy(previous, contentServices(4, null), Environment.prod, null, CONTAINER_CLUSTER);
fail("Expected exception due to resources reduction");
}
catch (IllegalArgumentException expected) {
assertEquals("resources-reduction: Size reduction in 'default' is too large: " +
"To guard against mistakes, the new max nodes must be at least 50% of the current nodes. " +
"Current nodes: 10, new nodes: 4. " +
ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction),
Exceptions.toMessageString(expected));
}
}
@Test
void testSizeReductionValidationMinimalDecreaseIsAllowed() {
ValidationTester tester = new ValidationTester(30);
VespaModel previous = tester.deploy(null, contentServices(3, null), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(2, null), Environment.prod, null, CONTAINER_CLUSTER);
}
@Test
void testOverridingSizeReductionValidation() {
ValidationTester tester = new ValidationTester(33);
VespaModel previous = tester.deploy(null, contentServices(30, null), Environment.prod, null, CONTAINER_CLUSTER).getFirst();
tester.deploy(previous, contentServices(14, null), Environment.prod, resourcesReductionOverride, CONTAINER_CLUSTER); // Allowed due to override
}
private void assertResourceReductionException(Exception e, NodeResources currentResources, NodeResources newResources) {
assertEquals("resources-reduction: Resource reduction in 'default' is too large: " +
"To guard against mistakes, the new max resources must be at least 50% of the current max " +
"resources in all dimensions. " +
"Current: " + currentResources.withBandwidthGbps(0) +
", new: " + newResources.withBandwidthGbps(0) + ". " +
ValidationOverrides.toAllowMessage(ValidationId.resourcesReduction),
Exceptions.toMessageString(e));
}
private static String containerServices(int nodes, NodeResources resources) {
String resourcesStr = resources == null ?
"" :
String.format(" ",
resources.vcpu(), resources.memoryGb(), resources.diskGb());
return "" +
" " +
" " +
resourcesStr +
" " +
" " +
"";
}
private static String contentServices(int nodes, NodeResources resources) {
String resourcesStr = resources == null ?
"" :
String.format(" ",
resources.vcpu(), resources.memoryGb(), resources.diskGb());
return "" +
" " +
" 1" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
resourcesStr +
" " +
" " +
"";
}
private static final String resourcesReductionOverride =
"\n" +
" resources-reduction\n" +
"\n";
}