diff --git a/docs/reference/index-modules/allocation.asciidoc b/docs/reference/index-modules/allocation.asciidoc index 4e56b2cd049..79e5d8ecdcc 100644 --- a/docs/reference/index-modules/allocation.asciidoc +++ b/docs/reference/index-modules/allocation.asciidoc @@ -37,7 +37,7 @@ curl -XPUT localhost:9200/test/_settings -d '{ }' -------------------------------------------------- -`index.routing.allocation.require.*` can be used to +`index.routing.allocation.require.*` can be used to specify a number of rules, all of which MUST match in order for a shard to be allocated to a node. This is in contrast to `include` which will include a node if ANY rule matches. @@ -117,14 +117,14 @@ Once enabled, Elasticsearch uses two watermarks to decide whether shards should be allocated or can remain on the node. `cluster.routing.allocation.disk.watermark.low` controls the low -watermark for disk usage. It defaults to 0.70, meaning ES will not +watermark for disk usage. It defaults to 70%, meaning ES will not allocate new shards to nodes once they have more than 70% disk used. It can also be set to an absolute byte value (like 500mb) to prevent ES from allocating shards if less than the configured amount of space is available. `cluster.routing.allocation.disk.watermark.high` controls the high -watermark. It defaults to 0.85, meaning ES will attempt to relocate +watermark. It defaults to 85%, meaning ES will attempt to relocate shards to another node if the node disk usage rises above 85%. It can also be set to an absolute byte value (similar to the low watermark) to relocate shards once less than the configured amount of space is diff --git a/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java b/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java index 63d7aced429..d928524aec6 100644 --- a/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java +++ b/src/main/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDecider.java @@ -28,6 +28,7 @@ import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.RatioValue; import org.elasticsearch.node.settings.NodeSettingsService; import java.util.Map; @@ -109,8 +110,8 @@ public class DiskThresholdDecider extends AllocationDecider { @Inject public DiskThresholdDecider(Settings settings, NodeSettingsService nodeSettingsService) { super(settings); - String lowWatermark = settings.get(CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK, "0.7"); - String highWatermark = settings.get(CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK, "0.85"); + String lowWatermark = settings.get(CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK, "70%"); + String highWatermark = settings.get(CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK, "85%"); if (!validWatermarkSetting(lowWatermark)) { throw new ElasticsearchParseException("Unable to parse low watermark: [" + lowWatermark + "]"); @@ -307,8 +308,8 @@ public class DiskThresholdDecider extends AllocationDecider { */ public double thresholdPercentageFromWatermark(String watermark) { try { - return 100.0 * Double.parseDouble(watermark); - } catch (NumberFormatException ex) { + return RatioValue.parseRatioValue(watermark).getAsPercent(); + } catch (ElasticsearchParseException ex) { return 100.0; } } @@ -331,12 +332,9 @@ public class DiskThresholdDecider extends AllocationDecider { */ public boolean validWatermarkSetting(String watermark) { try { - double w = Double.parseDouble(watermark); - if (w < 0 || w > 1.0) { - return false; - } + RatioValue.parseRatioValue(watermark); return true; - } catch (NumberFormatException e) { + } catch (ElasticsearchParseException e) { try { ByteSizeValue.parseBytesSizeValue(watermark); return true; diff --git a/src/main/java/org/elasticsearch/common/settings/ImmutableSettings.java b/src/main/java/org/elasticsearch/common/settings/ImmutableSettings.java index bedf7ab75c7..d55ad7bcd43 100644 --- a/src/main/java/org/elasticsearch/common/settings/ImmutableSettings.java +++ b/src/main/java/org/elasticsearch/common/settings/ImmutableSettings.java @@ -388,6 +388,16 @@ public class ImmutableSettings implements Settings { return MemorySizeValue.parseBytesSizeValueOrHeapRatio(get(settings, defaultValue)); } + @Override + public RatioValue getAsRatio(String setting, String defaultValue) throws SettingsException { + return RatioValue.parseRatioValue(get(setting, defaultValue)); + } + + @Override + public RatioValue getAsRatio(String[] settings, String defaultValue) throws SettingsException { + return RatioValue.parseRatioValue(get(settings, defaultValue)); + } + @Override public SizeValue getAsSize(String setting, SizeValue defaultValue) throws SettingsException { return parseSizeValue(get(setting), defaultValue); diff --git a/src/main/java/org/elasticsearch/common/settings/Settings.java b/src/main/java/org/elasticsearch/common/settings/Settings.java index f5a0cc3dd6a..aed56be3038 100644 --- a/src/main/java/org/elasticsearch/common/settings/Settings.java +++ b/src/main/java/org/elasticsearch/common/settings/Settings.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.unit.RatioValue; import org.elasticsearch.common.unit.SizeValue; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; @@ -212,6 +213,20 @@ public interface Settings extends ToXContent { */ ByteSizeValue getAsMemory(String[] setting, String defaultValue) throws SettingsException; + /** + * Returns the setting value (as a RatioValue) associated with the setting key. Provided values can + * either be a percentage value (eg. 23%), or expressed as a floating point number (eg. 0.23). If + * it does not exist, parses the default value provided. + */ + RatioValue getAsRatio(String setting, String defaultValue) throws SettingsException; + + /** + * Returns the setting value (as a RatioValue) associated with the setting key. Provided values can + * either be a percentage value (eg. 23%), or expressed as a floating point number (eg. 0.23). If + * it does not exist, parses the default value provided. + */ + RatioValue getAsRatio(String[] settings, String defaultValue) throws SettingsException; + /** * Returns the setting value (as size) associated with the setting key. If it does not exists, * returns the default value provided. diff --git a/src/main/java/org/elasticsearch/common/unit/RatioValue.java b/src/main/java/org/elasticsearch/common/unit/RatioValue.java new file mode 100644 index 00000000000..55132a2ba7e --- /dev/null +++ b/src/main/java/org/elasticsearch/common/unit/RatioValue.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.unit; + +import org.elasticsearch.ElasticsearchParseException; + +/** + * Utility class to represent ratio and percentage values between 0 and 100 + */ +public class RatioValue { + private final double percent; + + public RatioValue(double percent) { + this.percent = percent; + } + + public double getAsRatio() { + return this.percent / 100.0; + } + + public double getAsPercent() { + return this.percent; + } + + public String toString() { + return this.percent + "%"; + } + + /** + * Parses the provided string as a {@link RatioValue}, the string can + * either be in percentage format (eg. 73.5%), or a floating-point ratio + * format (eg. 0.735) + */ + public static RatioValue parseRatioValue(String sValue) { + if (sValue.endsWith("%")) { + final String percentAsString = sValue.substring(0, sValue.length() - 1); + try { + final double percent = Double.parseDouble(percentAsString); + if (percent < 0 || percent > 100) { + throw new ElasticsearchParseException("Percentage should be in [0-100], got " + percentAsString); + } + return new RatioValue(Math.abs(percent)); + } catch (NumberFormatException e) { + throw new ElasticsearchParseException("Failed to parse [" + percentAsString + "] as a double", e); + } + } else { + try { + double ratio = Double.parseDouble(sValue); + if (ratio < 0 || ratio > 1.0) { + throw new ElasticsearchParseException("Ratio should be in [0-1.0], got " + ratio); + } + return new RatioValue(100.0 * Math.abs(ratio)); + } catch (NumberFormatException e) { + throw new ElasticsearchParseException("Invalid ratio or percentage: [" + sValue + "]"); + } + + } + } +} diff --git a/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java b/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java index 68059120700..19ccf8d9676 100644 --- a/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java +++ b/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/DiskThresholdDeciderTests.java @@ -155,7 +155,7 @@ public class DiskThresholdDeciderTests extends ElasticsearchAllocationTestCase { // node2 now should not have new shards allocated to it, but shards can remain diskSettings = settingsBuilder() .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_DISK_THRESHOLD_ENABLED, true) - .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK, 0.6) + .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK, "60%") .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK, 0.7).build(); deciders = new AllocationDeciders(ImmutableSettings.EMPTY, @@ -430,7 +430,7 @@ public class DiskThresholdDeciderTests extends ElasticsearchAllocationTestCase { Settings diskSettings = settingsBuilder() .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_DISK_THRESHOLD_ENABLED, true) .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK, 0.7) - .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK, 0.71).build(); + .put(DiskThresholdDecider.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK, "71%").build(); Map usages = new HashMap<>(); usages.put("node1", new DiskUsage("node1", 100, 31)); // 69% used diff --git a/src/test/java/org/elasticsearch/common/unit/RatioValueTests.java b/src/test/java/org/elasticsearch/common/unit/RatioValueTests.java new file mode 100644 index 00000000000..d9a04726e4a --- /dev/null +++ b/src/test/java/org/elasticsearch/common/unit/RatioValueTests.java @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.unit; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import static org.hamcrest.Matchers.is; + +/** + * Tests for the {@link RatioValue} class + */ +public class RatioValueTests extends ElasticsearchTestCase { + + @Test + public void testParsing() { + assertThat(RatioValue.parseRatioValue("100%").toString(), is("100.0%")); + assertThat(RatioValue.parseRatioValue("0%").toString(), is("0.0%")); + assertThat(RatioValue.parseRatioValue("-0%").toString(), is("0.0%")); + assertThat(RatioValue.parseRatioValue("15.1%").toString(), is("15.1%")); + assertThat(RatioValue.parseRatioValue("0.1%").toString(), is("0.1%")); + assertThat(RatioValue.parseRatioValue("1.0").toString(), is("100.0%")); + assertThat(RatioValue.parseRatioValue("0").toString(), is("0.0%")); + assertThat(RatioValue.parseRatioValue("-0").toString(), is("0.0%")); + assertThat(RatioValue.parseRatioValue("0.0").toString(), is("0.0%")); + assertThat(RatioValue.parseRatioValue("-0.0").toString(), is("0.0%")); + assertThat(RatioValue.parseRatioValue("0.151").toString(), is("15.1%")); + assertThat(RatioValue.parseRatioValue("0.001").toString(), is("0.1%")); + } + + @Test + public void testNegativeCase() { + testInvalidRatio("100.0001%"); + testInvalidRatio("-0.1%"); + testInvalidRatio("1a0%"); + testInvalidRatio("2"); + testInvalidRatio("-0.01"); + testInvalidRatio("0.1.0"); + testInvalidRatio("five"); + testInvalidRatio("1/2"); + } + + public void testInvalidRatio(String r) { + try { + RatioValue.parseRatioValue(r); + fail("Value: [" + r + "] should be an invalid ratio"); + } catch (ElasticsearchParseException e) { + // success + } + } +}