diff options
Diffstat (limited to 'config-model')
26 files changed, 532 insertions, 117 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/LogserverContainer.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/LogserverContainer.java index 880e0e8c574..081f655b369 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/LogserverContainer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/LogserverContainer.java @@ -15,9 +15,11 @@ import com.yahoo.vespa.model.container.component.AccessLogComponent.CompressionT */ public class LogserverContainer extends Container { - public LogserverContainer(AbstractConfigProducer parent, FeatureFlags featureFlags, boolean isHostedVespa) { + public LogserverContainer(AbstractConfigProducer<?> parent, FeatureFlags featureFlags, boolean isHostedVespa) { super(parent, featureFlags, "" + 0, 0, isHostedVespa); - addComponent(new AccessLogComponent(AccessLogType.jsonAccessLog, CompressionType.GZIP, ((LogserverContainerCluster) parent).getName(), true)); + LogserverContainerCluster cluster = (LogserverContainerCluster) parent; + addComponent(new AccessLogComponent( + cluster, AccessLogType.jsonAccessLog, CompressionType.GZIP, cluster.getName(), true)); } @Override diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java index 2ad3bc8f84a..4cb3dde0833 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java @@ -55,7 +55,7 @@ public class ClusterControllerContainer extends Container implements "com.yahoo.vespa.clustercontroller.apps.clustercontroller.StateRestApiV2Handler", "/cluster/v2/*", CLUSTERCONTROLLER_BUNDLE); - addComponent(new AccessLogComponent(AccessLogComponent.AccessLogType.jsonAccessLog, + addComponent(new AccessLogComponent(containerCluster().orElse(null), AccessLogComponent.AccessLogType.jsonAccessLog, AccessLogComponent.CompressionType.GZIP, "controller", deployState.isHosted())); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java index 2bb12654fc4..11039528fc7 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/QuotaValidator.java @@ -11,6 +11,7 @@ import java.math.BigDecimal; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.logging.Logger; import java.util.stream.Collectors; /** @@ -19,6 +20,8 @@ import java.util.stream.Collectors; * @author ogronnesby */ public class QuotaValidator extends Validator { + private static final Logger log = Logger.getLogger(QuotaValidator.class.getName()); + @Override public void validate(VespaModel model, DeployState deployState) { var quota = deployState.getProperties().quota(); @@ -33,9 +36,13 @@ public class QuotaValidator extends Validator { .mapToDouble(clusterCapacity -> clusterCapacity.nodeResources().cost() * clusterCapacity.nodes()) .sum(); - if (budget.doubleValue() < spend) { - throwBudgetExceeded(spend, budget, systemName); + if (Math.abs(spend) < 0.01) { + log.warning("Deploying application " + model.applicationPackage().getApplicationId() + " with zero budget use. This is suspicious, but not blocked"); + return; } + + throwIfBudgetNegative(spend, budget, systemName); + throwIfBudgetExceeded(spend, budget, systemName); } /** Check that all clusters in the application do not exceed the quota max cluster size. */ @@ -57,8 +64,20 @@ public class QuotaValidator extends Validator { } } - private void throwBudgetExceeded(double spend, BigDecimal budget, SystemName systemName) { - var message = String.format(Locale.US, "Hourly spend for maximum specified resources ($%.2f) exceeds budget from quota ($%.2f)!", spend, budget); + private void throwIfBudgetNegative(double spend, BigDecimal budget, SystemName systemName) { + if (budget.doubleValue() < 0) { + throwBudgetException("Please free up some capacity! This deployment's quota use is ($%.2f) and reserved quota is below zero! ($%.2f)", spend, budget, systemName); + } + } + + private void throwIfBudgetExceeded(double spend, BigDecimal budget, SystemName systemName) { + if (budget.doubleValue() < spend) { + throwBudgetException("Please free up some capacity! This deployment's quota use ($%.2f) exceeds reserved quota ($%.2f)!", spend, budget, systemName); + } + } + + private void throwBudgetException(String formatMessage, double spend, BigDecimal budget, SystemName systemName) { + var message = String.format(Locale.US, formatMessage, spend, budget); var messageWithSystem = (systemName.equals(SystemName.Public) ? "" : systemName.value() + ": ") + message; throw new IllegalArgumentException(messageWithSystem); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java index 0add9f243fe..a462cb4fdb3 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidator.java @@ -39,7 +39,7 @@ public class NodeResourceChangeValidator implements ChangeValidator { } private boolean changeRequiresRestart(NodeResources currentResources, NodeResources nextResources) { - return currentResources.memoryGb() != nextResources.memoryGb(); + return !currentResources.equals(nextResources); } private Optional<NodeResources> resourcesOf(ClusterSpec.Id clusterId, VespaModel model) { diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java index 8b85c0a46ae..a6d0dad5ff6 100755 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java @@ -534,7 +534,7 @@ public abstract class ContainerCluster<CONTAINER extends Container> var compressionType = isHostedVespa && deployState.featureFlags().enableZstdCompressionAccessLog() ? AccessLogComponent.CompressionType.ZSTD : AccessLogComponent.CompressionType.GZIP; - addComponent(new AccessLogComponent(AccessLogComponent.AccessLogType.jsonAccessLog, compressionType, getName(), isHostedVespa)); + addComponent(new AccessLogComponent(this, AccessLogComponent.AccessLogType.jsonAccessLog, compressionType, getName(), isHostedVespa)); } @Override diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java index ffb7b876fa2..2905471b02e 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java @@ -3,9 +3,12 @@ package com.yahoo.vespa.model.container.component; import com.yahoo.container.core.AccessLogConfig; import com.yahoo.container.core.AccessLogConfig.FileHandler.CompressionFormat; -import com.yahoo.container.logging.VespaAccessLog; import com.yahoo.container.logging.JSONAccessLog; +import com.yahoo.container.logging.VespaAccessLog; import com.yahoo.osgi.provider.model.ComponentModel; +import com.yahoo.vespa.model.container.ContainerCluster; + +import java.util.OptionalInt; /** * @author Tony Vaagenes @@ -13,6 +16,7 @@ import com.yahoo.osgi.provider.model.ComponentModel; */ public final class AccessLogComponent extends SimpleComponent implements AccessLogConfig.Producer { + public enum AccessLogType { queryAccessLog, yApacheAccessLog, jsonAccessLog } public enum CompressionType { GZIP, ZSTD } @@ -22,10 +26,11 @@ public final class AccessLogComponent extends SimpleComponent implements AccessL private final boolean isHostedVespa; private final String symlinkName; private final CompressionType compressionType; + private final int queueSize; - public AccessLogComponent(AccessLogType logType, CompressionType compressionType, String clusterName, boolean isHostedVespa) + public AccessLogComponent(ContainerCluster<?> cluster, AccessLogType logType, CompressionType compressionType, String clusterName, boolean isHostedVespa) { - this(logType, compressionType, + this(cluster, logType, compressionType, String.format("logs/vespa/qrs/%s.%s.%s", capitalize(logType.name()), clusterName, "%Y%m%d%H%M%S"), null, null, isHostedVespa, capitalize(logType.name()) + "." + clusterName); @@ -35,7 +40,8 @@ public final class AccessLogComponent extends SimpleComponent implements AccessL return name.substring(0, 1).toUpperCase() + name.substring(1); } - public AccessLogComponent(AccessLogType logType, + public AccessLogComponent(ContainerCluster<?> cluster, + AccessLogType logType, CompressionType compressionType, String fileNamePattern, String rotationInterval, @@ -50,11 +56,19 @@ public final class AccessLogComponent extends SimpleComponent implements AccessL this.isHostedVespa = isHostedVespa; this.symlinkName = symlinkName; this.compressionType = compressionType; + this.queueSize = queueSize(cluster).orElse(-1); if (fileNamePattern == null) throw new RuntimeException("File name pattern required when configuring access log."); } + private static OptionalInt queueSize(ContainerCluster<?> cluster) { + if (cluster == null) return OptionalInt.empty(); + double vcpu = cluster.vcpu().orElse(0); + if (vcpu <= 0) return OptionalInt.empty(); + return OptionalInt.of((int) Math.max(4096, Math.ceil(vcpu * 256.0))); + } + private static String accessLogClass(AccessLogType logType) { switch (logType) { case queryAccessLog: @@ -84,6 +98,9 @@ public final class AccessLogComponent extends SimpleComponent implements AccessL } else if (isHostedVespa) { builder.compressOnRotation(true); } + if (queueSize >= 0) { + builder.queueSize(queueSize); + } switch (compressionType) { case GZIP: builder.compressionFormat(CompressionFormat.GZIP); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java index bf279c5c364..9977e955f85 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java @@ -5,18 +5,37 @@ package com.yahoo.vespa.model.container.component; import com.yahoo.container.logging.ConnectionLog; import com.yahoo.container.logging.ConnectionLogConfig; import com.yahoo.osgi.provider.model.ComponentModel; +import com.yahoo.vespa.model.container.ContainerCluster; + +import java.util.OptionalInt; public class ConnectionLogComponent extends SimpleComponent implements ConnectionLogConfig.Producer { private final String clusterName; + private final int queueSize; + + public ConnectionLogComponent(ContainerCluster<?> cluster, Class<? extends ConnectionLog> cls) { + this(cluster, cls, cluster.getName()); + } - public ConnectionLogComponent(Class<? extends ConnectionLog> cls, String clusterName) { + public ConnectionLogComponent(ContainerCluster<?> cluster, Class<? extends ConnectionLog> cls, String clusterName) { super(new ComponentModel(cls.getName(), null, "jdisc_http_service", null)); this.clusterName = clusterName; + this.queueSize = queueSize(cluster).orElse(-1); + } + + private static OptionalInt queueSize(ContainerCluster<?> cluster) { + if (cluster == null) return OptionalInt.empty(); + double vcpu = cluster.vcpu().orElse(0); + if (vcpu <= 0) return OptionalInt.empty(); + return OptionalInt.of((int) Math.max(4096, Math.ceil(vcpu * 512.0))); } @Override public void getConfig(ConnectionLogConfig.Builder builder) { builder.cluster(clusterName); + if (queueSize >= 0) { + builder.queueSize(queueSize); + } } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java index 901c422f843..b6ebd73b442 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java @@ -25,7 +25,7 @@ public class JettyHttpServer extends SimpleComponent implements ServerConfig.Pro private final ContainerCluster<?> cluster; private final boolean isHostedVespa; private final List<ConnectorFactory> connectorFactories = new ArrayList<>(); - private final boolean enableJdiscConnectionLog; + private volatile boolean enableJdiscConnectionLog; public JettyHttpServer(ComponentId id, ContainerCluster<?> cluster, ModelContext.FeatureFlags featureFlags, boolean isHostedVespa) { super(new ComponentModel( @@ -54,6 +54,8 @@ public class JettyHttpServer extends SimpleComponent implements ServerConfig.Pro connectorFactories.remove(connectorFactory); } + public void enableConnectionLog(boolean enabled) { this.enableJdiscConnectionLog = enabled; } + public List<ConnectorFactory> getConnectorFactories() { return Collections.unmodifiableList(connectorFactories); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java index 936b9f1c851..81e96760684 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java @@ -46,19 +46,18 @@ public class AccessLogBuilder { private static class DomBuilder extends VespaDomBuilder.DomConfigProducerBuilder<AccessLogComponent> { private final AccessLogType accessLogType; private final boolean isHostedVespa; - private final boolean isConfigserver; - public DomBuilder(AccessLogType accessLogType, boolean isHostedVespa, boolean isConfigserver) { + public DomBuilder(AccessLogType accessLogType, boolean isHostedVespa) { this.accessLogType = accessLogType; this.isHostedVespa = isHostedVespa; - this.isConfigserver = isConfigserver; } @Override protected AccessLogComponent doBuild(DeployState deployState, AbstractConfigProducer<?> ancestor, Element spec) { return new AccessLogComponent( + (ContainerCluster<?>) ancestor, accessLogType, - compressionType(spec, deployState, isHostedVespa, isConfigserver), + compressionType(spec, deployState, isHostedVespa), fileNamePattern(spec), rotationInterval(spec), compressOnRotation(spec), @@ -83,14 +82,13 @@ public class AccessLogBuilder { return nullIfEmpty(spec.getAttribute("fileNamePattern")); } - private static CompressionType compressionType(Element spec, DeployState deployState, boolean isHostedVespa, boolean isConfigserver) { + private static CompressionType compressionType(Element spec, DeployState deployState, boolean isHostedVespa) { CompressionType fallback; - if (isHostedVespa && (isConfigserver || deployState.featureFlags().enableZstdCompressionAccessLog())) { + if (isHostedVespa && deployState.featureFlags().enableZstdCompressionAccessLog()) { fallback = CompressionType.ZSTD; } else { fallback = CompressionType.GZIP; } - if (isConfigserver && isHostedVespa) return CompressionType.ZSTD; return Optional.ofNullable(spec.getAttribute("compressionType")) .filter(value -> !value.isBlank()) .map(value -> { @@ -120,7 +118,7 @@ public class AccessLogBuilder { } } - public static Optional<AccessLogComponent> buildIfNotDisabled(DeployState deployState, ContainerCluster<?> cluster, Element accessLogSpec, boolean isConfigserver) { + public static Optional<AccessLogComponent> buildIfNotDisabled(DeployState deployState, ContainerCluster<?> cluster, Element accessLogSpec) { AccessLogTypeLiteral typeLiteral = getOptionalAttribute(accessLogSpec, "type"). map(AccessLogTypeLiteral::fromAttributeValue). @@ -130,9 +128,6 @@ public class AccessLogBuilder { return Optional.empty(); } boolean hosted = cluster.isHostedVespa(); - if (hosted && isConfigserver && logType != AccessLogType.jsonAccessLog) { - return Optional.empty(); // Only enable JSON access logging for hosted configserver/controller - } - return Optional.of(new DomBuilder(logType, hosted, isConfigserver).build(deployState, cluster, accessLogSpec)); + return Optional.of(new DomBuilder(logType, hosted).build(deployState, cluster, accessLogSpec)); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java index e7db4ab0564..0c5375edf71 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java @@ -2,8 +2,12 @@ package com.yahoo.vespa.model.container.xml; import com.yahoo.config.model.ConfigModelContext; +import com.yahoo.config.model.deploy.DeployState; +import com.yahoo.container.logging.FileConnectionLog; import com.yahoo.vespa.model.container.ApplicationContainerCluster; import com.yahoo.vespa.model.container.ContainerModel; +import com.yahoo.vespa.model.container.component.AccessLogComponent; +import com.yahoo.vespa.model.container.component.ConnectionLogComponent; import com.yahoo.vespa.model.container.configserver.ConfigserverCluster; import com.yahoo.vespa.model.container.configserver.option.CloudConfigOptions; import org.w3c.dom.Element; @@ -22,8 +26,6 @@ public class ConfigServerContainerModelBuilder extends ContainerModelBuilder { this.options = options; } - @Override protected boolean isConfigserver() { return true; } - @Override public void doBuild(ContainerModel model, Element spec, ConfigModelContext modelContext) { ConfigserverCluster cluster = new ConfigserverCluster(modelContext.getParentProducer(), "configserver", @@ -36,7 +38,31 @@ public class ConfigServerContainerModelBuilder extends ContainerModelBuilder { // in ConfigModelContext.DeployState.properties are not set) @Override protected void addStatusHandlers(ApplicationContainerCluster cluster, boolean isHostedVespa) { - super.addStatusHandlers(cluster, options.hostedVespa().orElse(Boolean.FALSE)); + super.addStatusHandlers(cluster, isHosted()); + } + + // Override access log configuration for hosted configserver/controller + @Override + protected void addAccessLogs(DeployState deployState, ApplicationContainerCluster cluster, Element spec) { + if (isHosted()){ + cluster.addComponent( + new AccessLogComponent( + cluster, AccessLogComponent.AccessLogType.jsonAccessLog, AccessLogComponent.CompressionType.ZSTD, + "logs/vespa/configserver/access-json.log.%Y%m%d%H%M%S", null, true, true, "access-json.log")); + cluster.addComponent(new ConnectionLogComponent(cluster, FileConnectionLog.class)); + } else { + super.addAccessLogs(deployState, cluster, spec); + } + } + + @Override + protected void addHttp(DeployState deployState, Element spec, ApplicationContainerCluster cluster, ConfigModelContext context) { + super.addHttp(deployState, spec, cluster, context); + if (isHosted()) { + cluster.getHttp().getHttpServer().get().enableConnectionLog(true); + } } + /** Note: using {@link CloudConfigOptions} as {@link DeployState#isHosted()} returns <em>false</em> for hosted configserver/controller */ + private boolean isHosted() { return options.hostedVespa().orElse(Boolean.FALSE); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 07ccf3808fd..8735b76100d 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -147,8 +147,6 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { this.httpServerEnabled = networking == Networking.enable; } - protected boolean isConfigserver() { return false; } - @Override public List<ConfigModelId> handlesElements() { return configModelIds; @@ -340,11 +338,11 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { addConfiguredComponents(deployState, cluster, spec, "server"); } - private void addAccessLogs(DeployState deployState, ApplicationContainerCluster cluster, Element spec) { + protected void addAccessLogs(DeployState deployState, ApplicationContainerCluster cluster, Element spec) { List<Element> accessLogElements = getAccessLogElements(spec); for (Element accessLog : accessLogElements) { - AccessLogBuilder.buildIfNotDisabled(deployState, cluster, accessLog, isConfigserver()).ifPresent(cluster::addComponent); + AccessLogBuilder.buildIfNotDisabled(deployState, cluster, accessLog).ifPresent(cluster::addComponent); } if (accessLogElements.isEmpty() && deployState.getAccessLoggingEnabledByDefault()) @@ -352,9 +350,9 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { // Add connection log if access log is configured if (cluster.getAllComponents().stream().anyMatch(component -> component instanceof AccessLogComponent)) { - cluster.addComponent(new ConnectionLogComponent(FileConnectionLog.class, cluster.getName())); + cluster.addComponent(new ConnectionLogComponent(cluster, FileConnectionLog.class)); } else { - cluster.addComponent(new ConnectionLogComponent(VoidConnectionLog.class, cluster.getName())); + cluster.addComponent(new ConnectionLogComponent(cluster, VoidConnectionLog.class)); } } @@ -363,7 +361,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { } - private void addHttp(DeployState deployState, Element spec, ApplicationContainerCluster cluster, ConfigModelContext context) { + protected void addHttp(DeployState deployState, Element spec, ApplicationContainerCluster cluster, ConfigModelContext context) { Element httpElement = XML.getChild(spec, "http"); if (httpElement != null) { cluster.setHttp(buildHttp(deployState, cluster, httpElement)); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java index 8d6127970c8..66ec0d81947 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java @@ -19,12 +19,14 @@ import org.w3c.dom.Element; public class ClusterControllerConfig extends AbstractConfigProducer<ClusterControllerConfig> implements FleetcontrollerConfig.Producer { public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<ClusterControllerConfig> { - String clusterName; - ModelElement clusterElement; + private final String clusterName; + private final ModelElement clusterElement; + private final ResourceLimits resourceLimits; - public Builder(String clusterName, ModelElement clusterElement) { + public Builder(String clusterName, ModelElement clusterElement, ResourceLimits resourceLimits) { this.clusterName = clusterName; this.clusterElement = clusterElement; + this.resourceLimits = resourceLimits; } @Override @@ -51,27 +53,29 @@ public class ClusterControllerConfig extends AbstractConfigProducer<ClusterContr tuning.childAsDouble("min-storage-up-ratio"), bucketSplittingMinimumBits, minNodeRatioPerGroup, - enableClusterFeedBlock); + enableClusterFeedBlock, + resourceLimits); } else { return new ClusterControllerConfig(ancestor, clusterName, null, null, null, null, null, null, bucketSplittingMinimumBits, minNodeRatioPerGroup, - enableClusterFeedBlock); + enableClusterFeedBlock, resourceLimits); } } } - String clusterName; - Duration initProgressTime; - Duration transitionTime; - Long maxPrematureCrashes; - Duration stableStateTimePeriod; - Double minDistributorUpRatio; - Double minStorageUpRatio; - Integer minSplitBits; - private Double minNodeRatioPerGroup; - private boolean enableClusterFeedBlock = false; + private final String clusterName; + private final Duration initProgressTime; + private final Duration transitionTime; + private final Long maxPrematureCrashes; + private final Duration stableStateTimePeriod; + private final Double minDistributorUpRatio; + private final Double minStorageUpRatio; + private final Integer minSplitBits; + private final Double minNodeRatioPerGroup; + private final boolean enableClusterFeedBlock; + private final ResourceLimits resourceLimits; // TODO refactor; too many args private ClusterControllerConfig(AbstractConfigProducer parent, @@ -84,7 +88,8 @@ public class ClusterControllerConfig extends AbstractConfigProducer<ClusterContr Double minStorageUpRatio, Integer minSplitBits, Double minNodeRatioPerGroup, - boolean enableClusterFeedBlock) { + boolean enableClusterFeedBlock, + ResourceLimits resourceLimits) { super(parent, "fleetcontroller"); this.clusterName = clusterName; @@ -97,6 +102,7 @@ public class ClusterControllerConfig extends AbstractConfigProducer<ClusterContr this.minSplitBits = minSplitBits; this.minNodeRatioPerGroup = minNodeRatioPerGroup; this.enableClusterFeedBlock = enableClusterFeedBlock; + this.resourceLimits = resourceLimits; } @Override @@ -139,18 +145,7 @@ public class ClusterControllerConfig extends AbstractConfigProducer<ClusterContr builder.min_node_ratio_per_group(minNodeRatioPerGroup); } builder.enable_cluster_feed_block(enableClusterFeedBlock); - setDefaultClusterFeedBlockLimits(builder); + resourceLimits.getConfig(builder); } - private static void setDefaultClusterFeedBlockLimits(FleetcontrollerConfig.Builder builder) { - // TODO: Override these based on resource-limits in services.xml (if they are specified). - // TODO: Choose other defaults when this is default enabled. - // Note: The resource categories must match the ones used in host info reporting - // between content nodes and cluster controller: - // storage/src/vespa/storage/persistence/filestorage/service_layer_host_info_reporter.cpp - builder.cluster_feed_block_limit.put("memory", 0.79); - builder.cluster_feed_block_limit.put("disk", 0.79); - builder.cluster_feed_block_limit.put("attribute-enum-store", 0.89); - builder.cluster_feed_block_limit.put("attribute-multi-value", 0.89); - } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterResourceLimits.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterResourceLimits.java new file mode 100644 index 00000000000..5324ee171ec --- /dev/null +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterResourceLimits.java @@ -0,0 +1,103 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.content; + +import com.yahoo.vespa.model.builder.xml.dom.ModelElement; +import com.yahoo.vespa.model.content.cluster.DomResourceLimitsBuilder; + +import java.util.Optional; +import java.util.function.Consumer; + +/** + * Class tracking the feed block resource limits for a content cluster. + * + * This includes the limits used by the cluster controller and the content nodes (proton). + * + * @author geirst + */ +public class ClusterResourceLimits { + + private final ResourceLimits clusterControllerLimits; + private final ResourceLimits contentNodeLimits; + + private ClusterResourceLimits(Builder builder) { + clusterControllerLimits = builder.ctrlBuilder.build(); + contentNodeLimits = builder.nodeBuilder.build(); + } + + public ResourceLimits getClusterControllerLimits() { + return clusterControllerLimits; + } + + public ResourceLimits getContentNodeLimits() { + return contentNodeLimits; + } + + public static class Builder { + + private ResourceLimits.Builder ctrlBuilder = new ResourceLimits.Builder(); + private ResourceLimits.Builder nodeBuilder = new ResourceLimits.Builder(); + + public ClusterResourceLimits build(ModelElement clusterElem) { + + ModelElement tuningElem = clusterElem.childByPath("tuning"); + if (tuningElem != null) { + ctrlBuilder = DomResourceLimitsBuilder.createBuilder(tuningElem); + } + + ModelElement protonElem = clusterElem.childByPath("engine.proton"); + if (protonElem != null) { + nodeBuilder = DomResourceLimitsBuilder.createBuilder(protonElem); + } + + deriveLimits(); + return new ClusterResourceLimits(this); + } + + public void setClusterControllerBuilder(ResourceLimits.Builder builder) { + ctrlBuilder = builder; + } + + public void setContentNodeBuilder(ResourceLimits.Builder builder) { + nodeBuilder = builder; + } + + public ClusterResourceLimits build() { + deriveLimits(); + return new ClusterResourceLimits(this); + } + + private void deriveLimits() { + deriveClusterControllerLimit(ctrlBuilder.getDiskLimit(), nodeBuilder.getDiskLimit(), ctrlBuilder::setDiskLimit); + deriveClusterControllerLimit(ctrlBuilder.getMemoryLimit(), nodeBuilder.getMemoryLimit(), ctrlBuilder::setMemoryLimit); + + deriveContentNodeLimit(nodeBuilder.getDiskLimit(), ctrlBuilder.getDiskLimit(), nodeBuilder::setDiskLimit); + deriveContentNodeLimit(nodeBuilder.getMemoryLimit(), ctrlBuilder.getMemoryLimit(), nodeBuilder::setMemoryLimit); + } + + private void deriveClusterControllerLimit(Optional<Double> clusterControllerLimit, + Optional<Double> contentNodeLimit, + Consumer<Double> setter) { + if (!clusterControllerLimit.isPresent()) { + contentNodeLimit.ifPresent(limit -> + // TODO: emit warning when using cluster controller resource limits are default enabled. + setter.accept(limit)); + } + } + + private void deriveContentNodeLimit(Optional<Double> contentNodeLimit, + Optional<Double> clusterControllerLimit, + Consumer<Double> setter) { + if (!contentNodeLimit.isPresent()) { + clusterControllerLimit.ifPresent(limit -> + setter.accept(calcContentNodeLimit(limit))); + } + } + + private double calcContentNodeLimit(double clusterControllerLimit) { + // Note that validation in the range [0.0-1.0] is handled by the rnc schema. + return clusterControllerLimit + ((1.0 - clusterControllerLimit) / 2); + } + + } + +} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java index dd29df61f35..7ce72b138c1 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java @@ -79,13 +79,15 @@ public class ContentSearchCluster extends AbstractConfigProducer<SearchCluster> private final Map<String, NewDocumentType> documentDefinitions; private final Set<NewDocumentType> globallyDistributedDocuments; private final boolean combined; + private final ResourceLimits resourceLimits; public Builder(Map<String, NewDocumentType> documentDefinitions, Set<NewDocumentType> globallyDistributedDocuments, - boolean combined) { + boolean combined, ResourceLimits resourceLimits) { this.documentDefinitions = documentDefinitions; this.globallyDistributedDocuments = globallyDistributedDocuments; this.combined = combined; + this.resourceLimits = resourceLimits; } @Override @@ -106,10 +108,7 @@ public class ContentSearchCluster extends AbstractConfigProducer<SearchCluster> if (tuning != null) { search.setTuning(new DomSearchTuningBuilder().build(deployState, search, tuning.getXml())); } - ModelElement protonElem = clusterElem.childByPath("engine.proton"); - if (protonElem != null) { - search.setResourceLimits(DomResourceLimitsBuilder.build(protonElem)); - } + search.setResourceLimits(resourceLimits); buildAllStreamingSearchClusters(deployState, clusterElem, clusterName, search); buildIndexedSearchCluster(deployState, clusterElem, clusterName, search); @@ -190,7 +189,7 @@ public class ContentSearchCluster extends AbstractConfigProducer<SearchCluster> } } - private ContentSearchCluster(AbstractConfigProducer parent, + private ContentSearchCluster(AbstractConfigProducer<?> parent, String clusterName, ModelContext.FeatureFlags featureFlags, Map<String, NewDocumentType> documentDefinitions, @@ -233,8 +232,8 @@ public class ContentSearchCluster extends AbstractConfigProducer<SearchCluster> NamedSchema searchDefinition = schemaDefinitionXMLHandler.getResponsibleSearchDefinition(deployState.getSchemas()); if (searchDefinition == null) - throw new RuntimeException("Search definition parsing error or file does not exist: '" + - schemaDefinitionXMLHandler.getName() + "'"); + throw new RuntimeException("Schema '" + schemaDefinitionXMLHandler.getName() + "' referenced in " + + this + " does not exist"); // TODO: remove explicit building of user configs when the complete content model is built using builders. sc.getLocalSDS().add(new AbstractSearchCluster.SchemaSpec(searchDefinition, @@ -445,4 +444,8 @@ public class ContentSearchCluster extends AbstractConfigProducer<SearchCluster> public IndexedSearchCluster getIndexed() { return indexedCluster; } public boolean hasIndexedCluster() { return indexedCluster != null; } public String getClusterName() { return clusterName; } + + @Override + public String toString() { return "content cluster '" + clusterName + "'"; } + } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java index 28e8c36d202..e96ba47c6b3 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java @@ -1,16 +1,17 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.content; +import com.yahoo.vespa.config.content.FleetcontrollerConfig; import com.yahoo.vespa.config.search.core.ProtonConfig; import java.util.Optional; /** - * Class tracking resource limits for a content cluster with engine proton. + * Class tracking feed block resource limits used by a component in a content cluster (e.g. cluster controller or content node). * * @author geirst */ -public class ResourceLimits implements ProtonConfig.Producer { +public class ResourceLimits implements FleetcontrollerConfig.Producer, ProtonConfig.Producer { private final Optional<Double> diskLimit; private final Optional<Double> memoryLimit; @@ -20,6 +21,26 @@ public class ResourceLimits implements ProtonConfig.Producer { this.memoryLimit = builder.memoryLimit; } + public Optional<Double> getDiskLimit() { + return diskLimit; + } + + public Optional<Double> getMemoryLimit() { + return memoryLimit; + } + + @Override + public void getConfig(FleetcontrollerConfig.Builder builder) { + // TODO: Choose other defaults when this is default enabled. + // Note: The resource categories must match the ones used in host info reporting + // between content nodes and cluster controller: + // storage/src/vespa/storage/persistence/filestorage/service_layer_host_info_reporter.cpp + builder.cluster_feed_block_limit.put("memory", memoryLimit.orElse(0.79)); + builder.cluster_feed_block_limit.put("disk", diskLimit.orElse(0.79)); + builder.cluster_feed_block_limit.put("attribute-enum-store", 0.89); + builder.cluster_feed_block_limit.put("attribute-multi-value", 0.89); + } + @Override public void getConfig(ProtonConfig.Builder builder) { if (diskLimit.isPresent()) { @@ -39,11 +60,19 @@ public class ResourceLimits implements ProtonConfig.Producer { return new ResourceLimits(this); } + public Optional<Double> getDiskLimit() { + return diskLimit; + } + public Builder setDiskLimit(double diskLimit) { this.diskLimit = Optional.of(diskLimit); return this; } + public Optional<Double> getMemoryLimit() { + return memoryLimit; + } + public Builder setMemoryLimit(double memoryLimit) { this.memoryLimit = Optional.of(memoryLimit); return this; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java index a627e030156..44de4a1abec 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java @@ -38,6 +38,7 @@ import com.yahoo.vespa.model.container.Container; import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.ContainerModel; import com.yahoo.vespa.model.content.ClusterControllerConfig; +import com.yahoo.vespa.model.content.ClusterResourceLimits; import com.yahoo.vespa.model.content.ContentSearch; import com.yahoo.vespa.model.content.ContentSearchCluster; import com.yahoo.vespa.model.content.DistributionBitCalculator; @@ -134,11 +135,14 @@ public class ContentCluster extends AbstractConfigProducer implements ContentCluster c = new ContentCluster(context.getParentProducer(), getClusterId(contentElement), documentDefinitions, globallyDistributedDocuments, routingSelection, deployState.zone(), deployState.isHosted()); - c.clusterControllerConfig = new ClusterControllerConfig.Builder(getClusterId(contentElement), contentElement).build(deployState, c, contentElement.getXml()); + var resourceLimits = new ClusterResourceLimits.Builder().build(contentElement); + c.clusterControllerConfig = new ClusterControllerConfig.Builder(getClusterId(contentElement), + contentElement, + resourceLimits.getClusterControllerLimits()).build(deployState, c, contentElement.getXml()); c.search = new ContentSearchCluster.Builder(documentDefinitions, - globallyDistributedDocuments, - isCombined(getClusterId(contentElement), containers)) - .build(deployState, c, contentElement.getXml()); + globallyDistributedDocuments, + isCombined(getClusterId(contentElement), containers), + resourceLimits.getContentNodeLimits()).build(deployState, c, contentElement.getXml()); c.persistenceFactory = new EngineFactoryBuilder().build(contentElement, c); c.storageNodes = new StorageCluster.Builder().build(deployState, c, w3cContentElement); c.distributorNodes = new DistributorCluster.Builder(c).build(deployState, c, w3cContentElement); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java index 8e91f14238e..210f062f9b2 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java @@ -5,17 +5,17 @@ import com.yahoo.vespa.model.builder.xml.dom.ModelElement; import com.yahoo.vespa.model.content.ResourceLimits; /** - * Builder for resource limits for a content cluster with engine proton. + * Builder for feed block resource limits. * * @author geirst */ public class DomResourceLimitsBuilder { - public static ResourceLimits build(ModelElement contentXml) { + public static ResourceLimits.Builder createBuilder(ModelElement contentXml) { ResourceLimits.Builder builder = new ResourceLimits.Builder(); ModelElement resourceLimits = contentXml.child("resource-limits"); if (resourceLimits == null) { - return builder.build(); + return builder; } if (resourceLimits.child("disk") != null) { builder.setDiskLimit(resourceLimits.childAsDouble("disk")); @@ -23,7 +23,7 @@ public class DomResourceLimitsBuilder { if (resourceLimits.child("memory") != null) { builder.setMemoryLimit(resourceLimits.childAsDouble("memory")); } - return builder.build(); + return builder; } } diff --git a/config-model/src/main/resources/schema/content.rnc b/config-model/src/main/resources/schema/content.rnc index 5646bc72056..a48d38b9f2c 100644 --- a/config-model/src/main/resources/schema/content.rnc +++ b/config-model/src/main/resources/schema/content.rnc @@ -98,7 +98,8 @@ ClusterTuning = element tuning { ClusterControllerTuning? & Maintenance? & PersistenceThreads? & - MinNodeRatioPerGroup? + MinNodeRatioPerGroup? & + ResourceLimits? } Content = element content { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java index d3c41754c8d..6ad74231cae 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/QuotaValidatorTest.java @@ -47,7 +47,7 @@ public class QuotaValidatorTest { tester.deploy(null, getServices("testCluster", 10), Environment.prod, null); fail(); } catch (RuntimeException e) { - assertEquals("Hourly spend for maximum specified resources ($-.--) exceeds budget from quota ($-.--)!", + assertEquals("Please free up some capacity! This deployment's quota use ($-.--) exceeds reserved quota ($-.--)!", ValidationTester.censorNumbers(e.getMessage())); } } @@ -59,7 +59,20 @@ public class QuotaValidatorTest { tester.deploy(null, getServices("testCluster", 10), Environment.prod, null); fail(); } catch (RuntimeException e) { - assertEquals("publiccd: Hourly spend for maximum specified resources ($-.--) exceeds budget from quota ($-.--)!", + assertEquals("publiccd: Please free up some capacity! This deployment's quota use ($-.--) exceeds reserved quota ($-.--)!", + ValidationTester.censorNumbers(e.getMessage())); + } + } + + @Test + public void test_deploy_with_negative_budget() { + var quota = Quota.unlimited().withBudget(BigDecimal.valueOf(-1)); + var tester = new ValidationTester(10, false, new TestProperties().setHostedVespa(true).setQuota(quota).setZone(publicZone)); + try { + tester.deploy(null, getServices("testCluster", 10), Environment.prod, null); + fail(); + } catch (RuntimeException e) { + assertEquals("Please free up some capacity! This deployment's quota use is ($-.--) and reserved quota is below zero! ($--.--)", ValidationTester.censorNumbers(e.getMessage())); } } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidatorTest.java index ecf026e7d88..180e4913d5c 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidatorTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/NodeResourceChangeValidatorTest.java @@ -61,7 +61,7 @@ public class NodeResourceChangeValidatorTest { Clock.systemUTC().instant()); } - private static VespaModel model(int mem1, int mem2, int mem3, int mem4) { + private static VespaModel model(int mem1, int mem2, int cpu1, int cpu2) { var properties = new TestProperties(); properties.setHostedVespa(true); var deployState = new DeployState.Builder().properties(properties) @@ -82,7 +82,7 @@ public class NodeResourceChangeValidatorTest { " </container>\n" + " <content id='content1' version='1.0'>\n" + " <nodes count='3'>\n" + - " <resources vcpu='1' memory='" + mem3 + "Gb' disk='100Gb'/>" + + " <resources vcpu='" + cpu1 + "' memory='8Gb' disk='100Gb'/>" + " </nodes>\n" + " <documents>\n" + " <document mode='index' type='test'/>\n" + @@ -91,7 +91,7 @@ public class NodeResourceChangeValidatorTest { " </content>\n" + " <content id='content2' version='1.0'>\n" + " <nodes count='4'>\n" + - " <resources vcpu='1' memory='" + mem4 + "Gb' disk='100Gb'/>" + + " <resources vcpu='" + cpu2 + "' memory='8Gb' disk='100Gb'/>" + " </nodes>\n" + " <documents>\n" + " <document mode='streaming' type='test'/>\n" + diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java index 1d2dd94d6a3..56f09eefe82 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java @@ -86,6 +86,7 @@ public class AccessLogTest extends ContainerModelBuilderTestBase { AccessLogConfig.FileHandler fileHandlerConfig = config.fileHandler(); assertEquals("pattern", fileHandlerConfig.pattern()); assertEquals("interval", fileHandlerConfig.rotation()); + assertEquals(10000, fileHandlerConfig.queueSize()); } { // json @@ -96,6 +97,7 @@ public class AccessLogTest extends ContainerModelBuilderTestBase { AccessLogConfig.FileHandler fileHandlerConfig = config.fileHandler(); assertEquals("pattern", fileHandlerConfig.pattern()); assertEquals("interval", fileHandlerConfig.rotation()); + assertEquals(10000, fileHandlerConfig.queueSize()); } } @@ -114,6 +116,7 @@ public class AccessLogTest extends ContainerModelBuilderTestBase { assertNotNull(connectionLogComponent); ConnectionLogConfig config = root.getConfig(ConnectionLogConfig.class, "default/component/com.yahoo.container.logging.FileConnectionLog"); assertEquals("default", config.cluster()); + assertEquals(10000, config.queueSize()); } @Test diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JvmOptionsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JvmOptionsTest.java index 294df42bd77..60af25a3087 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JvmOptionsTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JvmOptionsTest.java @@ -78,11 +78,11 @@ public class JvmOptionsTest extends ContainerModelBuilderTestBase { } private static void verifyIgnoreJvmGCOptions(boolean isHosted) throws IOException, SAXException { - verifyIgnoreJvmGCOptionsIfJvmArgs("jvmargs", ContainerCluster.G1GC); - verifyIgnoreJvmGCOptionsIfJvmArgs( "jvm-options", "-XX:+UseG1GC"); + verifyIgnoreJvmGCOptionsIfJvmArgs("jvmargs", ContainerCluster.G1GC, isHosted); + verifyIgnoreJvmGCOptionsIfJvmArgs( "jvm-options", "-XX:+UseG1GC", isHosted); } - private static void verifyIgnoreJvmGCOptionsIfJvmArgs(String jvmOptionsName, String expectedGC) throws IOException, SAXException { + private static void verifyIgnoreJvmGCOptionsIfJvmArgs(String jvmOptionsName, String expectedGC, boolean isHosted) throws IOException, SAXException { String servicesXml = "<container version='1.0'>" + " <nodes jvm-gc-options='-XX:+UseG1GC' " + jvmOptionsName + "='-XX:+UseParNewGC'>" + @@ -95,6 +95,7 @@ public class JvmOptionsTest extends ContainerModelBuilderTestBase { VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder() .applicationPackage(applicationPackage) .deployLogger(logger) + .properties(new TestProperties().setHostedVespa(isHosted)) .build()); QrStartConfig.Builder qrStartBuilder = new QrStartConfig.Builder(); model.getConfig(qrStartBuilder, "container/container.0"); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ClusterResourceLimitsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ClusterResourceLimitsTest.java new file mode 100644 index 00000000000..bc830c079d0 --- /dev/null +++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ClusterResourceLimitsTest.java @@ -0,0 +1,101 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.model.content; + +import org.junit.Test; + +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * @author geirst + */ +public class ClusterResourceLimitsTest { + + private static class Fixture { + ResourceLimits.Builder ctrlBuilder = new ResourceLimits.Builder(); + ResourceLimits.Builder nodeBuilder = new ResourceLimits.Builder(); + + public Fixture ctrlDisk(double limit) { + ctrlBuilder.setDiskLimit(limit); + return this; + } + public Fixture ctrlMemory(double limit) { + ctrlBuilder.setMemoryLimit(limit); + return this; + } + public Fixture nodeDisk(double limit) { + nodeBuilder.setDiskLimit(limit); + return this; + } + public Fixture nodeMemory(double limit) { + nodeBuilder.setMemoryLimit(limit); + return this; + } + public ClusterResourceLimits build() { + var builder = new ClusterResourceLimits.Builder(); + builder.setClusterControllerBuilder(ctrlBuilder); + builder.setContentNodeBuilder(nodeBuilder); + return builder.build(); + } + } + + @Test + public void content_node_limits_are_derived_from_cluster_controller_limits_if_not_set() { + assertLimits(0.6, 0.7, 0.8, 0.85, + new Fixture().ctrlDisk(0.6).ctrlMemory(0.7)); + assertLimits(0.6, null, 0.8, null, + new Fixture().ctrlDisk(0.6)); + assertLimits(null, 0.7, null, 0.85, + new Fixture().ctrlMemory(0.7)); + } + + @Test + public void content_node_limits_can_be_set_explicit() { + assertLimits(0.6, 0.7, 0.9, 0.95, + new Fixture().ctrlDisk(0.6).ctrlMemory(0.7).nodeDisk(0.9).nodeMemory(0.95)); + assertLimits(0.6, null, 0.9, null, + new Fixture().ctrlDisk(0.6).nodeDisk(0.9)); + assertLimits(null, 0.7, null, 0.95, + new Fixture().ctrlMemory(0.7).nodeMemory(0.95)); + } + + @Test + public void cluster_controller_limits_are_equal_to_content_node_limits_if_not_set() { + assertLimits(0.9, 0.95, 0.9, 0.95, + new Fixture().nodeDisk(0.9).nodeMemory(0.95)); + assertLimits(0.9, null, 0.9, null, + new Fixture().nodeDisk(0.9)); + assertLimits(null, 0.95, null, 0.95, + new Fixture().nodeMemory(0.95)); + } + + @Test + public void limits_are_derived_from_the_other_if_not_set() { + assertLimits(0.6, 0.95, 0.8, 0.95, + new Fixture().ctrlDisk(0.6).nodeMemory(0.95)); + assertLimits(0.9, 0.7, 0.9, 0.85, + new Fixture().ctrlMemory(0.7).nodeDisk(0.9)); + } + + private void assertLimits(Double expCtrlDisk, Double expCtrlMemory, Double expNodeDisk, Double expNodeMemory, Fixture f) { + var limits = f.build(); + assertLimits(expCtrlDisk, expCtrlMemory, limits.getClusterControllerLimits()); + assertLimits(expNodeDisk, expNodeMemory, limits.getContentNodeLimits()); + } + + private void assertLimits(Double expDisk, Double expMemory, ResourceLimits limits) { + assertLimit(expDisk, limits.getDiskLimit()); + assertLimit(expMemory, limits.getMemoryLimit()); + } + + private void assertLimit(Double expLimit, Optional<Double> actLimit) { + if (expLimit == null) { + assertFalse(actLimit.isPresent()); + } else { + assertEquals(expLimit, actLimit.get(), 0.00001); + } + } + +} diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java index 3415044b088..bc60908e268 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java @@ -69,15 +69,30 @@ public class ContentSearchClusterTest { } private static ProtonConfig getProtonConfig(ContentCluster cluster) { - ProtonConfig.Builder protonCfgBuilder = new ProtonConfig.Builder(); - cluster.getSearch().getConfig(protonCfgBuilder); - return new ProtonConfig(protonCfgBuilder); + var builder = new ProtonConfig.Builder(); + cluster.getSearch().getConfig(builder); + return new ProtonConfig(builder); } - private static void assertProtonResourceLimits(double expDiskLimit, double expMemoryLimits, String clusterXml) throws Exception { - ProtonConfig cfg = getProtonConfig(createCluster(clusterXml)); + private static void assertProtonResourceLimits(double expDiskLimit, double expMemoryLimit, String clusterXml) throws Exception { + assertProtonResourceLimits(expDiskLimit, expMemoryLimit, createCluster(clusterXml)); + } + + private static void assertProtonResourceLimits(double expDiskLimit, double expMemoryLimit, ContentCluster cluster) { + var cfg = getProtonConfig(cluster); assertEquals(expDiskLimit, cfg.writefilter().disklimit(), EPSILON); - assertEquals(expMemoryLimits, cfg.writefilter().memorylimit(), EPSILON); + assertEquals(expMemoryLimit, cfg.writefilter().memorylimit(), EPSILON); + } + + private static void assertClusterControllerResourceLimits(double expDiskLimit, double expMemoryLimit, String clusterXml) throws Exception { + assertClusterControllerResourceLimits(expDiskLimit, expMemoryLimit, createCluster(clusterXml)); + } + + private static void assertClusterControllerResourceLimits(double expDiskLimit, double expMemoryLimit, ContentCluster cluster) { + var limits = getFleetcontrollerConfig(cluster).cluster_feed_block_limit(); + assertEquals(4, limits.size()); + assertEquals(expDiskLimit, limits.get("disk"), EPSILON); + assertEquals(expMemoryLimit, limits.get("memory"), EPSILON); } @Test @@ -105,6 +120,19 @@ public class ContentSearchClusterTest { } @Test + public void cluster_controller_resource_limits_can_be_set() throws Exception { + assertClusterControllerResourceLimits(0.92, 0.93, + new ContentClusterBuilder().clusterControllerDiskLimit(0.92).clusterControllerMemoryLimit(0.93).getXml()); + } + + @Test + public void resource_limits_are_derived_from_the_other_if_not_specified() throws Exception { + var cluster = createCluster(new ContentClusterBuilder().clusterControllerDiskLimit(0.5).protonMemoryLimit(0.95).getXml()); + assertProtonResourceLimits(0.75, 0.95, cluster); + assertClusterControllerResourceLimits(0.5, 0.95, cluster); + } + + @Test public void requireThatGloballyDistributedDocumentTypeIsTaggedAsSuch() throws Exception { ProtonConfig cfg = getProtonConfig(createClusterWithGlobalType()); assertEquals(2, cfg.documentdb().size()); @@ -149,8 +177,9 @@ public class ContentSearchClusterTest { } private static FleetcontrollerConfig getFleetcontrollerConfig(ContentCluster cluster) { - FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder(); + var builder = new FleetcontrollerConfig.Builder(); cluster.getConfig(builder); + cluster.getClusterControllerConfig().getConfig(builder); builder.cluster_name("unknown"); builder.index(0); builder.zookeeper_server("unknown"); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java index 01bbffce360..3a59f35ce2e 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java @@ -10,6 +10,7 @@ import com.yahoo.vespa.model.builder.xml.dom.ModelElement; import org.junit.Test; import org.w3c.dom.Document; +import static com.yahoo.config.model.test.TestUtil.joinLines; import static org.junit.Assert.assertEquals; public class FleetControllerClusterTest { @@ -19,8 +20,11 @@ public class FleetControllerClusterTest { var deployState = new DeployState.Builder().properties( new TestProperties().enableFeedBlockInDistributor(enableFeedBlockInDistributor)).build(); MockRoot root = new MockRoot("", deployState); - return new ClusterControllerConfig.Builder("storage", new ModelElement(doc.getDocumentElement())).build(root.getDeployState(), root, - new ModelElement(doc.getDocumentElement()).getXml()); + var clusterElement = new ModelElement(doc.getDocumentElement()); + return new ClusterControllerConfig.Builder("storage", + clusterElement, + new ClusterResourceLimits.Builder().build(clusterElement).getClusterControllerLimits()). + build(root.getDeployState(), root, clusterElement.getXml()); } private ClusterControllerConfig parse(String xml) { @@ -94,15 +98,43 @@ public class FleetControllerClusterTest { assertEquals(0.0, config.min_node_ratio_per_group(), 0.01); } + @Test public void default_cluster_feed_block_limits_are_set() { - var config = getConfigForBasicCluster(); + assertLimits(0.79, 0.79, getConfigForBasicCluster()); + } + + @Test + public void resource_limits_can_be_set_in_tuning() { + assertLimits(0.6, 0.7, getConfigForResourceLimitsTuning(0.6, 0.7)); + assertLimits(0.6, 0.79, getConfigForResourceLimitsTuning(0.6, null)); + assertLimits(0.79, 0.7, getConfigForResourceLimitsTuning(null, 0.7)); + } + + private static double DELTA = 0.00001; + + private void assertLimits(double expDisk, double expMemory, FleetcontrollerConfig config) { var limits = config.cluster_feed_block_limit(); assertEquals(4, limits.size()); - assertEquals(0.79, limits.get("memory"), 0.0001); - assertEquals(0.79, limits.get("disk"), 0.0001); - assertEquals(0.89, limits.get("attribute-enum-store"), 0.0001); - assertEquals(0.89, limits.get("attribute-multi-value"), 0.0001); + assertEquals(expDisk, limits.get("disk"), DELTA); + assertEquals(expMemory, limits.get("memory"), DELTA); + assertEquals(0.89, limits.get("attribute-enum-store"), DELTA); + assertEquals(0.89, limits.get("attribute-multi-value"), DELTA); + } + + private FleetcontrollerConfig getConfigForResourceLimitsTuning(Double diskLimit, Double memoryLimit) { + FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder(); + parse(joinLines("<cluster id=\"test\">", + "<documents/>", + "<tuning>", + " <resource-limits>", + (diskLimit != null ? (" <disk>" + diskLimit + "</disk>") : ""), + (memoryLimit != null ? (" <memory>" + memoryLimit + "</memory>") : ""), + " </resource-limits>", + "</tuning>" + + "</cluster>")). + getConfig(builder); + return new FleetcontrollerConfig(builder); } @Test diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java index 866c03d82f0..491326fdc9c 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java @@ -26,6 +26,8 @@ public class ContentClusterBuilder { private Optional<String> dispatchXml = Optional.empty(); private Optional<Double> protonDiskLimit = Optional.empty(); private Optional<Double> protonMemoryLimit = Optional.empty(); + private Optional<Double> clusterControllerDiskLimit = Optional.empty(); + private Optional<Double> clusterControllerMemoryLimit = Optional.empty(); public ContentClusterBuilder() { } @@ -67,13 +69,23 @@ public class ContentClusterBuilder { return this; } - public ContentClusterBuilder protonDiskLimit(double diskLimit) { - protonDiskLimit = Optional.of(diskLimit); + public ContentClusterBuilder protonDiskLimit(double limit) { + protonDiskLimit = Optional.of(limit); return this; } - public ContentClusterBuilder protonMemoryLimit(double memoryLimit) { - protonMemoryLimit = Optional.of(memoryLimit); + public ContentClusterBuilder protonMemoryLimit(double limit) { + protonMemoryLimit = Optional.of(limit); + return this; + } + + public ContentClusterBuilder clusterControllerDiskLimit(double limit) { + clusterControllerDiskLimit = Optional.of(limit); + return this; + } + + public ContentClusterBuilder clusterControllerMemoryLimit(double limit) { + clusterControllerMemoryLimit = Optional.of(limit); return this; } @@ -88,14 +100,17 @@ public class ContentClusterBuilder { " <engine>", " <proton>", " <searchable-copies>" + searchableCopies + "</searchable-copies>", - getResourceLimitsXml(" "), + getProtonResourceLimitsXml(" "), " </proton>", " </engine>"); if (dispatchXml.isPresent()) { xml += dispatchXml.get(); } - return xml + groupXml + - "</content>"; + xml += groupXml; + xml += joinLines(" <tuning>", + getTuningResourceLimitsXml(" "), + " </tuning>"); + return xml + "</content>"; } private static String getSimpleGroupXml() { @@ -104,11 +119,19 @@ public class ContentClusterBuilder { " </group>"); } - private String getResourceLimitsXml(String indent) { - if (protonDiskLimit.isPresent() || protonMemoryLimit.isPresent()) { + private String getProtonResourceLimitsXml(String indent) { + return getResourceLimitsXml(indent, protonDiskLimit, protonMemoryLimit); + } + + private String getTuningResourceLimitsXml(String indent) { + return getResourceLimitsXml(indent, clusterControllerDiskLimit, clusterControllerMemoryLimit); + } + + private String getResourceLimitsXml(String indent, Optional<Double> diskLimit, Optional<Double> memoryLimit) { + if (diskLimit.isPresent() || memoryLimit.isPresent()) { String xml = joinLines(indent + "<resource-limits>", - getXmlLine("disk", protonDiskLimit, indent + " "), - getXmlLine("memory", protonMemoryLimit, indent + " "), + getXmlLine("disk", diskLimit, indent + " "), + getXmlLine("memory", memoryLimit, indent + " "), indent + "</resource-limits>"); return xml; } |