Skip to content

Commit 2286118

Browse files
Fail start of non-data node if node has data (#37347)
* Fail start of non-data node if node has data Check that nodes started with node.data=false cannot start if they have shard data to avoid (old) indexes being resurrected into the cluster in red status. Issue #27073
1 parent 2a7b7cc commit 2286118

File tree

4 files changed

+159
-2
lines changed

4 files changed

+159
-2
lines changed

server/src/main/java/org/elasticsearch/env/NodeEnvironment.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.elasticsearch.monitor.fs.FsInfo;
6060
import org.elasticsearch.monitor.fs.FsProbe;
6161
import org.elasticsearch.monitor.jvm.JvmInfo;
62+
import org.elasticsearch.node.Node;
6263

6364
import java.io.Closeable;
6465
import java.io.IOException;
@@ -309,6 +310,11 @@ public NodeEnvironment(Settings settings, Environment environment) throws IOExce
309310
if (DiscoveryNode.isMasterNode(settings) || DiscoveryNode.isDataNode(settings)) {
310311
ensureAtomicMoveSupported(nodePaths);
311312
}
313+
314+
if (DiscoveryNode.isDataNode(settings) == false) {
315+
ensureNoShardData(nodePaths);
316+
}
317+
312318
success = true;
313319
} finally {
314320
if (success == false) {
@@ -1030,6 +1036,38 @@ private static void ensureAtomicMoveSupported(final NodePath[] nodePaths) throws
10301036
}
10311037
}
10321038

1039+
private void ensureNoShardData(final NodePath[] nodePaths) throws IOException {
1040+
List<Path> shardDataPaths = new ArrayList<>();
1041+
for (NodePath nodePath : nodePaths) {
1042+
Path indicesPath = nodePath.indicesPath;
1043+
if (Files.isDirectory(indicesPath)) {
1044+
try (DirectoryStream<Path> indexStream = Files.newDirectoryStream(indicesPath)) {
1045+
for (Path indexPath : indexStream) {
1046+
if (Files.isDirectory(indexPath)) {
1047+
try (Stream<Path> shardStream = Files.list(indexPath)) {
1048+
shardStream.filter(this::isShardPath)
1049+
.map(Path::toAbsolutePath)
1050+
.forEach(shardDataPaths::add);
1051+
}
1052+
}
1053+
}
1054+
}
1055+
}
1056+
}
1057+
1058+
if (shardDataPaths.isEmpty() == false) {
1059+
throw new IllegalStateException("Node is started with "
1060+
+ Node.NODE_DATA_SETTING.getKey()
1061+
+ "=false, but has shard data: "
1062+
+ shardDataPaths);
1063+
}
1064+
}
1065+
1066+
private boolean isShardPath(Path path) {
1067+
return Files.isDirectory(path)
1068+
&& path.getFileName().toString().chars().allMatch(Character::isDigit);
1069+
}
1070+
10331071
/**
10341072
* Resolve the custom path for a index's shard.
10351073
* Uses the {@code IndexMetaData.SETTING_DATA_PATH} setting to determine
@@ -1140,3 +1178,4 @@ private static void tryWriteTempFile(Path path) throws IOException {
11401178
}
11411179
}
11421180
}
1181+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.env;
21+
22+
import org.elasticsearch.common.settings.Settings;
23+
import org.elasticsearch.node.Node;
24+
import org.elasticsearch.test.ESIntegTestCase;
25+
import org.elasticsearch.test.InternalTestCluster;
26+
27+
import static org.hamcrest.Matchers.containsString;
28+
import static org.hamcrest.Matchers.startsWith;
29+
30+
@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0)
31+
public class NodeEnvironmentIT extends ESIntegTestCase {
32+
public void testStartFailureOnDataForNonDataNode() throws Exception {
33+
final String indexName = "test-fail-on-data";
34+
35+
logger.info("--> starting one node");
36+
internalCluster().startNode();
37+
38+
logger.info("--> creating index");
39+
prepareCreate(indexName, Settings.builder()
40+
.put("index.number_of_shards", 1)
41+
.put("index.number_of_replicas", 0)
42+
).get();
43+
final String indexUUID = resolveIndex(indexName).getUUID();
44+
45+
logger.info("--> indexing a simple document");
46+
client().prepareIndex(indexName, "type1", "1").setSource("field1", "value1").get();
47+
48+
logger.info("--> restarting the node with node.data=true");
49+
internalCluster().restartRandomDataNode();
50+
51+
logger.info("--> restarting the node with node.data=false");
52+
IllegalStateException ex = expectThrows(IllegalStateException.class,
53+
"Node started with node.data=false and existing shard data must fail",
54+
() ->
55+
internalCluster().restartRandomDataNode(new InternalTestCluster.RestartCallback() {
56+
@Override
57+
public Settings onNodeStopped(String nodeName) {
58+
return Settings.builder().put(Node.NODE_DATA_SETTING.getKey(), false).build();
59+
}
60+
}));
61+
assertThat(ex.getMessage(), containsString(indexUUID));
62+
assertThat(ex.getMessage(),
63+
startsWith("Node is started with "
64+
+ Node.NODE_DATA_SETTING.getKey()
65+
+ "=false, but has shard data"));
66+
}
67+
}

server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.elasticsearch.index.Index;
3232
import org.elasticsearch.index.IndexSettings;
3333
import org.elasticsearch.index.shard.ShardId;
34+
import org.elasticsearch.node.Node;
3435
import org.elasticsearch.test.ESTestCase;
3536
import org.elasticsearch.test.IndexSettingsModule;
3637

@@ -52,6 +53,7 @@
5253
import static org.hamcrest.Matchers.containsString;
5354
import static org.hamcrest.Matchers.empty;
5455
import static org.hamcrest.Matchers.not;
56+
import static org.hamcrest.Matchers.startsWith;
5557

5658
@LuceneTestCase.SuppressFileSystems("ExtrasFS") // TODO: fix test to allow extras
5759
public class NodeEnvironmentTests extends ESTestCase {
@@ -470,6 +472,47 @@ public void testExistingTempFiles() throws IOException {
470472
}
471473
}
472474

475+
public void testEnsureNoShardData() throws IOException {
476+
Settings settings = buildEnvSettings(Settings.EMPTY);
477+
Index index = new Index("test", "testUUID");
478+
479+
try (NodeEnvironment env = newNodeEnvironment(settings)) {
480+
for (Path path : env.indexPaths(index)) {
481+
Files.createDirectories(path.resolve(MetaDataStateFormat.STATE_DIR_NAME));
482+
}
483+
}
484+
485+
// build settings using same path.data as original but with node.data=false
486+
Settings noDataSettings = Settings.builder()
487+
.put(settings)
488+
.put(Node.NODE_DATA_SETTING.getKey(), false).build();
489+
490+
String shardDataDirName = Integer.toString(randomInt(10));
491+
Path shardPath;
492+
493+
// test that we can create data=false env with only meta information
494+
try (NodeEnvironment env = newNodeEnvironment(noDataSettings)) {
495+
for (Path path : env.indexPaths(index)) {
496+
Files.createDirectories(path.resolve(shardDataDirName));
497+
}
498+
shardPath = env.indexPaths(index)[0];
499+
}
500+
501+
IllegalStateException ex = expectThrows(IllegalStateException.class,
502+
"Must fail creating NodeEnvironment on a data path that has shard data if node.data=false",
503+
() -> newNodeEnvironment(noDataSettings).close());
504+
505+
assertThat(ex.getMessage(),
506+
containsString(shardPath.resolve(shardDataDirName).toAbsolutePath().toString()));
507+
assertThat(ex.getMessage(),
508+
startsWith("Node is started with "
509+
+ Node.NODE_DATA_SETTING.getKey()
510+
+ "=false, but has shard data"));
511+
512+
// test that we can create data=true env
513+
newNodeEnvironment(settings).close();
514+
}
515+
473516
/** Converts an array of Strings to an array of Paths, adding an additional child if specified */
474517
private Path[] stringsToPaths(String[] strings, String additional) {
475518
Path[] locations = new Path[strings.length];

test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,8 +1727,16 @@ private void restartNode(NodeAndClient nodeAndClient, RestartCallback callback)
17271727

17281728
removeExclusions(excludedNodeIds);
17291729

1730-
nodeAndClient.recreateNode(newSettings, () -> rebuildUnicastHostFiles(emptyList()));
1731-
nodeAndClient.startNode();
1730+
boolean success = false;
1731+
try {
1732+
nodeAndClient.recreateNode(newSettings, () -> rebuildUnicastHostFiles(emptyList()));
1733+
nodeAndClient.startNode();
1734+
success = true;
1735+
} finally {
1736+
if (success == false)
1737+
nodes.remove(nodeAndClient.name);
1738+
}
1739+
17321740
if (activeDisruptionScheme != null) {
17331741
activeDisruptionScheme.applyToNode(nodeAndClient.name, this);
17341742
}

0 commit comments

Comments
 (0)