Skip to content

Commit 2f5753f

Browse files
authored
Add unit tests for indices.recovery.max_bytes_per_sec default values (#83261) (#83283)
`indices.recovery.max_bytes_per_sec` has a default value that depends on multiple criteria that are well documented but not unit tested. This pull request introduces unit tests that verifies the current behavior so that future changes like #82819 are less likely to break things. Backport of #83261
1 parent 6f0dede commit 2f5753f

File tree

2 files changed

+246
-10
lines changed

2 files changed

+246
-10
lines changed

server/src/main/java/org/elasticsearch/indices/recovery/RecoverySettings.java

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import java.util.List;
3636
import java.util.Locale;
3737
import java.util.Map;
38-
import java.util.stream.Collectors;
3938

4039
import static org.elasticsearch.common.settings.Setting.parseInt;
4140

@@ -46,36 +45,57 @@ public class RecoverySettings {
4645

4746
private static final Logger logger = LogManager.getLogger(RecoverySettings.class);
4847

48+
/**
49+
* Undocumented setting, used to override the total physical available memory in tests
50+
**/
51+
// package private for tests
52+
static final Setting<ByteSizeValue> TOTAL_PHYSICAL_MEMORY_OVERRIDING_TEST_SETTING = Setting.byteSizeSetting(
53+
"recovery_settings.total_physical_memory_override",
54+
settings -> new ByteSizeValue(OsProbe.getInstance().getTotalPhysicalMemorySize()).getStringRep(),
55+
Property.NodeScope
56+
);
57+
58+
/**
59+
* Undocumented setting, used to override the current JVM version in tests
60+
**/
61+
// package private for tests
62+
static final Setting<JavaVersion> JAVA_VERSION_OVERRIDING_TEST_SETTING = new Setting<>(
63+
"recovery_settings.java_version_override",
64+
settings -> JavaVersion.current().toString(),
65+
JavaVersion::parse,
66+
Property.NodeScope
67+
);
68+
69+
public static final ByteSizeValue DEFAULT_MAX_BYTES_PER_SEC = new ByteSizeValue(40L, ByteSizeUnit.MB);
70+
4971
public static final Setting<ByteSizeValue> INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING = Setting.byteSizeSetting(
5072
"indices.recovery.max_bytes_per_sec",
5173
s -> {
52-
final ByteSizeValue defaultMaxBytesPerSec = new ByteSizeValue(40, ByteSizeUnit.MB);
5374
final List<DiscoveryNodeRole> roles = NodeRoleSettings.NODE_ROLES_SETTING.get(s);
54-
final List<DiscoveryNodeRole> dataRoles = roles.stream()
55-
.filter(DiscoveryNodeRole::canContainData)
56-
.collect(Collectors.toUnmodifiableList());
75+
final List<DiscoveryNodeRole> dataRoles = roles.stream().filter(DiscoveryNodeRole::canContainData).toList();
5776
if (dataRoles.isEmpty()) {
5877
// if the node is not a data node, this value doesn't matter, use the default
59-
return defaultMaxBytesPerSec.getStringRep();
78+
return DEFAULT_MAX_BYTES_PER_SEC.getStringRep();
6079
}
6180
if (dataRoles.stream()
6281
.allMatch(
6382
dn -> dn.equals(DiscoveryNodeRole.DATA_COLD_NODE_ROLE) || dn.equals(DiscoveryNodeRole.DATA_FROZEN_NODE_ROLE)
6483
) == false) {
6584
// the node is not a dedicated cold and/or frozen node, use the default
66-
return defaultMaxBytesPerSec.getStringRep();
85+
return DEFAULT_MAX_BYTES_PER_SEC.getStringRep();
6786
}
6887
/*
6988
* Now we are looking at a node that has a single data role, that data role is the cold data role, and the node does not
7089
* have the master role. In this case, we are going to set the recovery size as a function of the memory size. We are making
7190
* an assumption here that the size of the instance is correlated with I/O resources. That is we are assuming that the
7291
* larger the instance, the more disk and networking capacity it has available.
7392
*/
74-
if (JavaVersion.current().compareTo(JavaVersion.parse("14")) < 0) {
93+
final JavaVersion javaVersion = JAVA_VERSION_OVERRIDING_TEST_SETTING.get(s);
94+
if (javaVersion.compareTo(JavaVersion.parse("14")) < 0) {
7595
// prior to JDK 14, the JDK did not take into consideration container memory limits when reporting total system memory
76-
return defaultMaxBytesPerSec.getStringRep();
96+
return DEFAULT_MAX_BYTES_PER_SEC.getStringRep();
7797
}
78-
final ByteSizeValue totalPhysicalMemory = new ByteSizeValue(OsProbe.getInstance().getTotalPhysicalMemorySize());
98+
final ByteSizeValue totalPhysicalMemory = TOTAL_PHYSICAL_MEMORY_OVERRIDING_TEST_SETTING.get(s);
7999
final ByteSizeValue maxBytesPerSec;
80100
if (totalPhysicalMemory.compareTo(new ByteSizeValue(4, ByteSizeUnit.GB)) <= 0) {
81101
maxBytesPerSec = new ByteSizeValue(40, ByteSizeUnit.MB);
@@ -375,6 +395,10 @@ private void setMaxBytesPerSec(ByteSizeValue maxBytesPerSec) {
375395
}
376396
}
377397

398+
ByteSizeValue getMaxBytesPerSec() {
399+
return maxBytesPerSec;
400+
}
401+
378402
public int getMaxConcurrentFileChunks() {
379403
return maxConcurrentFileChunks;
380404
}

server/src/test/java/org/elasticsearch/indices/recovery/RecoverySettingsTests.java

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,28 @@
1010

1111
import org.elasticsearch.common.settings.ClusterSettings;
1212
import org.elasticsearch.common.settings.Settings;
13+
import org.elasticsearch.common.unit.ByteSizeUnit;
14+
import org.elasticsearch.common.unit.ByteSizeValue;
15+
import org.elasticsearch.core.Nullable;
1316
import org.elasticsearch.core.Releasable;
17+
import org.elasticsearch.jdk.JavaVersion;
1418
import org.elasticsearch.test.ESTestCase;
1519

20+
import java.util.ArrayList;
21+
import java.util.HashSet;
22+
import java.util.Objects;
23+
import java.util.Set;
24+
25+
import static org.elasticsearch.indices.recovery.RecoverySettings.DEFAULT_MAX_BYTES_PER_SEC;
26+
import static org.elasticsearch.indices.recovery.RecoverySettings.INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING;
1627
import static org.elasticsearch.indices.recovery.RecoverySettings.INDICES_RECOVERY_MAX_CONCURRENT_SNAPSHOT_FILE_DOWNLOADS;
1728
import static org.elasticsearch.indices.recovery.RecoverySettings.INDICES_RECOVERY_MAX_CONCURRENT_SNAPSHOT_FILE_DOWNLOADS_PER_NODE;
1829
import static org.elasticsearch.indices.recovery.RecoverySettings.INDICES_RECOVERY_USE_SNAPSHOTS_SETTING;
30+
import static org.elasticsearch.indices.recovery.RecoverySettings.JAVA_VERSION_OVERRIDING_TEST_SETTING;
31+
import static org.elasticsearch.indices.recovery.RecoverySettings.TOTAL_PHYSICAL_MEMORY_OVERRIDING_TEST_SETTING;
32+
import static org.elasticsearch.node.NodeRoleSettings.NODE_ROLES_SETTING;
1933
import static org.hamcrest.Matchers.containsString;
34+
import static org.hamcrest.Matchers.equalTo;
2035
import static org.hamcrest.Matchers.is;
2136
import static org.hamcrest.Matchers.notNullValue;
2237
import static org.hamcrest.Matchers.nullValue;
@@ -89,4 +104,201 @@ public void testMaxConcurrentSnapshotFileDownloadsPerNodeIsValidated() {
89104
)
90105
);
91106
}
107+
108+
public void testDefaultMaxBytesPerSecOnNonDataNode() {
109+
assertThat(
110+
"Non-data nodes have a default 40mb rate limit",
111+
nodeRecoverySettings().withRole(randomFrom("master", "ingest", "ml")).withRandomMemory().build().getMaxBytesPerSec(),
112+
equalTo(DEFAULT_MAX_BYTES_PER_SEC)
113+
);
114+
}
115+
116+
public void testMaxBytesPerSecOnNonDataNodeWithIndicesRecoveryMaxBytesPerSec() {
117+
final ByteSizeValue random = randomByteSizeValue();
118+
assertThat(
119+
"Non-data nodes should use the defined rate limit when set",
120+
nodeRecoverySettings().withRole(randomFrom("master", "ingest", "ml"))
121+
.withIndicesRecoveryMaxBytesPerSec(random)
122+
.withRandomMemory()
123+
.build()
124+
.getMaxBytesPerSec(),
125+
equalTo(random)
126+
);
127+
}
128+
129+
public void testDefaultMaxBytesPerSecOnDataNode() {
130+
assertThat(
131+
"Data nodes that are not dedicated to cold/frozen have a default 40mb rate limit",
132+
nodeRecoverySettings().withRole(randomFrom("data", "data_hot", "data_warm", "data_content"))
133+
.withRandomMemory()
134+
.build()
135+
.getMaxBytesPerSec(),
136+
equalTo(DEFAULT_MAX_BYTES_PER_SEC)
137+
);
138+
}
139+
140+
public void testMaxBytesPerSecOnDataNodeWithIndicesRecoveryMaxBytesPerSec() {
141+
final Set<String> roles = new HashSet<>(randomSubsetOf(randomIntBetween(1, 4), "data", "data_hot", "data_warm", "data_content"));
142+
roles.addAll(randomSubsetOf(Set.of("data_cold", "data_frozen")));
143+
final ByteSizeValue random = randomByteSizeValue();
144+
assertThat(
145+
"Data nodes that are not dedicated to cold/frozen should use the defined rate limit when set",
146+
nodeRecoverySettings().withRoles(roles)
147+
.withIndicesRecoveryMaxBytesPerSec(random)
148+
.withRandomMemory()
149+
.build()
150+
.getMaxBytesPerSec(),
151+
equalTo(random)
152+
);
153+
}
154+
155+
public void testDefaultMaxBytesPerSecOnColdOrFrozenNodeWithOldJvm() {
156+
assertThat(
157+
"Data nodes with only cold/frozen data roles have a default 40mb rate limit on Java version prior to 14",
158+
nodeRecoverySettings().withRoles(randomFrom(Set.of("data_cold"), Set.of("data_frozen"), Set.of("data_cold", "data_frozen")))
159+
.withJavaVersion(randomFrom("8", "9", "11"))
160+
.withRandomMemory()
161+
.build()
162+
.getMaxBytesPerSec(),
163+
equalTo(DEFAULT_MAX_BYTES_PER_SEC)
164+
);
165+
}
166+
167+
public void testDefaultMaxBytesPerSecOnColdOrFrozenNode() {
168+
final Set<String> dataRoles = randomFrom(Set.of("data_cold"), Set.of("data_frozen"), Set.of("data_cold", "data_frozen"));
169+
final String recentVersion = JavaVersion.current().compareTo(JavaVersion.parse("14")) < 0 ? "14" : null;
170+
{
171+
assertThat(
172+
"Dedicated cold/frozen data nodes with <= 4GB of RAM have a default 40mb rate limit",
173+
nodeRecoverySettings().withRoles(dataRoles)
174+
.withMemory(ByteSizeValue.ofBytes(randomLongBetween(1L, ByteSizeUnit.GB.toBytes(4L))))
175+
.withJavaVersion(recentVersion)
176+
.build()
177+
.getMaxBytesPerSec(),
178+
equalTo(new ByteSizeValue(40, ByteSizeUnit.MB))
179+
);
180+
}
181+
{
182+
assertThat(
183+
"Dedicated cold/frozen data nodes with 4GB < RAM <= 8GB have a default 60mb rate limit",
184+
nodeRecoverySettings().withRoles(dataRoles)
185+
.withMemory(ByteSizeValue.ofBytes(randomLongBetween(ByteSizeUnit.GB.toBytes(4L) + 1L, ByteSizeUnit.GB.toBytes(8L))))
186+
.withJavaVersion(recentVersion)
187+
.build()
188+
.getMaxBytesPerSec(),
189+
equalTo(new ByteSizeValue(60, ByteSizeUnit.MB))
190+
);
191+
}
192+
{
193+
assertThat(
194+
"Dedicated cold/frozen data nodes with 8GB < RAM <= 16GB have a default 90mb rate limit",
195+
nodeRecoverySettings().withRoles(dataRoles)
196+
.withMemory(ByteSizeValue.ofBytes(randomLongBetween(ByteSizeUnit.GB.toBytes(8L) + 1L, ByteSizeUnit.GB.toBytes(16L))))
197+
.withJavaVersion(recentVersion)
198+
.build()
199+
.getMaxBytesPerSec(),
200+
equalTo(new ByteSizeValue(90, ByteSizeUnit.MB))
201+
);
202+
}
203+
{
204+
assertThat(
205+
"Dedicated cold/frozen data nodes with 16GB < RAM <= 32GB have a default 90mb rate limit",
206+
nodeRecoverySettings().withRoles(dataRoles)
207+
.withMemory(ByteSizeValue.ofBytes(randomLongBetween(ByteSizeUnit.GB.toBytes(16L) + 1L, ByteSizeUnit.GB.toBytes(32L))))
208+
.withJavaVersion(recentVersion)
209+
.build()
210+
.getMaxBytesPerSec(),
211+
equalTo(new ByteSizeValue(125, ByteSizeUnit.MB))
212+
);
213+
}
214+
{
215+
assertThat(
216+
"Dedicated cold/frozen data nodes with RAM > 32GB have a default 250mb rate limit",
217+
nodeRecoverySettings().withRoles(dataRoles)
218+
.withMemory(ByteSizeValue.ofBytes(randomLongBetween(ByteSizeUnit.GB.toBytes(32L) + 1L, ByteSizeUnit.TB.toBytes(4L))))
219+
.withJavaVersion(recentVersion)
220+
.build()
221+
.getMaxBytesPerSec(),
222+
equalTo(new ByteSizeValue(250, ByteSizeUnit.MB))
223+
);
224+
}
225+
}
226+
227+
public void testMaxBytesPerSecOnColdOrFrozenNodeWithIndicesRecoveryMaxBytesPerSec() {
228+
final ByteSizeValue random = randomByteSizeValue();
229+
assertThat(
230+
"Dedicated cold/frozen data nodes should use the defined rate limit when set",
231+
nodeRecoverySettings().withRoles(randomFrom(Set.of("data_cold"), Set.of("data_frozen"), Set.of("data_cold", "data_frozen")))
232+
.withJavaVersion(JavaVersion.current().compareTo(JavaVersion.parse("14")) < 0 ? "14" : null)
233+
.withMemory(ByteSizeValue.ofBytes(randomLongBetween(1L, ByteSizeUnit.TB.toBytes(4L))))
234+
.withIndicesRecoveryMaxBytesPerSec(random)
235+
.build()
236+
.getMaxBytesPerSec(),
237+
equalTo(random)
238+
);
239+
}
240+
241+
public static ByteSizeValue randomByteSizeValue() {
242+
return new ByteSizeValue(randomLongBetween(0L, Long.MAX_VALUE >> 16));
243+
}
244+
245+
public static ByteSizeValue randomNonZeroByteSizeValue() {
246+
return new ByteSizeValue(randomLongBetween(1L, Long.MAX_VALUE >> 16));
247+
}
248+
249+
static NodeRecoverySettings nodeRecoverySettings() {
250+
return new NodeRecoverySettings();
251+
}
252+
253+
private static class NodeRecoverySettings {
254+
255+
private Set<String> roles;
256+
private ByteSizeValue physicalMemory;
257+
private @Nullable String javaVersion;
258+
private @Nullable ByteSizeValue indicesRecoveryMaxBytesPerSec;
259+
260+
NodeRecoverySettings withRole(String role) {
261+
this.roles = Set.of(Objects.requireNonNull(role));
262+
return this;
263+
}
264+
265+
NodeRecoverySettings withRoles(Set<String> roles) {
266+
this.roles = Objects.requireNonNull(roles);
267+
return this;
268+
}
269+
270+
NodeRecoverySettings withMemory(ByteSizeValue physicalMemory) {
271+
this.physicalMemory = Objects.requireNonNull(physicalMemory);
272+
return this;
273+
}
274+
275+
NodeRecoverySettings withRandomMemory() {
276+
return withMemory(ByteSizeValue.ofBytes(randomLongBetween(ByteSizeUnit.GB.toBytes(1L), ByteSizeUnit.TB.toBytes(4L))));
277+
}
278+
279+
NodeRecoverySettings withJavaVersion(String javaVersion) {
280+
this.javaVersion = javaVersion;
281+
return this;
282+
}
283+
284+
NodeRecoverySettings withIndicesRecoveryMaxBytesPerSec(ByteSizeValue indicesRecoveryMaxBytesPerSec) {
285+
this.indicesRecoveryMaxBytesPerSec = Objects.requireNonNull(indicesRecoveryMaxBytesPerSec);
286+
return this;
287+
}
288+
289+
RecoverySettings build() {
290+
final Settings.Builder settings = Settings.builder();
291+
settings.put(TOTAL_PHYSICAL_MEMORY_OVERRIDING_TEST_SETTING.getKey(), Objects.requireNonNull(physicalMemory));
292+
if (roles.isEmpty() == false) {
293+
settings.putList(NODE_ROLES_SETTING.getKey(), new ArrayList<>(roles));
294+
}
295+
if (javaVersion != null) {
296+
settings.put(JAVA_VERSION_OVERRIDING_TEST_SETTING.getKey(), javaVersion);
297+
}
298+
if (indicesRecoveryMaxBytesPerSec != null) {
299+
settings.put(INDICES_RECOVERY_MAX_BYTES_PER_SEC_SETTING.getKey(), indicesRecoveryMaxBytesPerSec);
300+
}
301+
return new RecoverySettings(settings.build(), new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS));
302+
}
303+
}
92304
}

0 commit comments

Comments
 (0)