Skip to content

Commit

Permalink
Improve error message in 8.x to 7.x downgrade (elastic#78644)
Browse files Browse the repository at this point in the history
In elastic#78638 we introduced a simple mechanism for blocking downgrades from
8.x to 7.x, but the exception message it generates is not very helpful:

    org.elasticsearch.bootstrap.StartupException: ElasticsearchException[failed to bind service]; nested: FileSystemException[{path.data}/nodes/0: Not a directory];

We can't fix earlier 7.x versions to do something better, but this
commit at least means that sufficiently recent 7.x versions will yield a
slightly more helpful message.
  • Loading branch information
DaveCTurner authored Oct 7, 2021
1 parent f75ba8b commit 0651f94
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 2 deletions.
39 changes: 39 additions & 0 deletions server/src/main/java/org/elasticsearch/env/NodeEnvironment.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.common.io.Channels;
import org.elasticsearch.core.CheckedFunction;
import org.elasticsearch.common.Randomness;
import org.elasticsearch.core.SuppressForbidden;
Expand Down Expand Up @@ -53,12 +54,19 @@
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -252,6 +260,17 @@ public NodeEnvironment(Settings settings, Environment environment) throws IOExce
NodeLock nodeLock = null;

try {
for (Path path : environment.dataFiles()) {
final Path nodesPath = path.resolve(NODES_FOLDER);
if (Files.exists(nodesPath) && Files.isDirectory(nodesPath) == false) {
throw new IllegalStateException(
"data path [" + path + "] is not compatible with Elasticsearch v" + Version.CURRENT +
", perhaps it has already been upgraded to a later version",
new IllegalStateException(
"[" + nodesPath + "] is a file which contains [" + readFileContents(nodesPath) + "]"));
}
}

sharedDataPath = environment.sharedDataFile();
IOException lastException = null;
int maxLocalStorageNodes = MAX_LOCAL_STORAGE_NODES_SETTING.get(settings);
Expand Down Expand Up @@ -326,6 +345,26 @@ public NodeEnvironment(Settings settings, Environment environment) throws IOExce
}
}

private static String readFileContents(Path nodesPath) throws IOException {
final int maxBytes = 256;

try (FileChannel fileChannel = FileChannel.open(nodesPath, StandardOpenOption.READ)) {
final ByteBuffer byteBuffer = ByteBuffer.allocate(maxBytes);
final int len = Channels.readFromFileChannel(fileChannel, 0, byteBuffer);
byteBuffer.flip();

final CharsetDecoder charsetDecoder = StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
try {
return charsetDecoder.decode(byteBuffer) + (len == maxBytes ? "..." : "");
} catch (CharacterCodingException e) {
return "<unreadable>";
}
}
}

/**
* Resolve a specific nodes/{node.id} path for the specified path and node lock id.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.gateway.MetadataStateFormat;
import org.elasticsearch.index.Index;
Expand All @@ -26,8 +26,11 @@
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.IndexSettingsModule;
import org.elasticsearch.test.NodeRoles;
import org.hamcrest.Matcher;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -39,13 +42,21 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.env.NodeEnvironment.NODES_FOLDER;
import static org.elasticsearch.test.NodeRoles.nonDataNode;
import static org.elasticsearch.test.NodeRoles.nonMasterNode;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;

Expand Down Expand Up @@ -526,6 +537,48 @@ public void testEnsureNoShardDataOrIndexMetadata() throws IOException {
verifyFailsOnShardData(noDataNoMasterSettings, indexPath, shardDataDirName);
}

public void testDowngradeFrom8xErrorMessage() throws IOException {
final Settings settings = buildEnvSettings(Settings.EMPTY);
final Environment environment = TestEnvironment.newEnvironment(settings);
// noinspection EmptyTryBlock we're just creating the directory structure
try (NodeEnvironment ignored = new NodeEnvironment(settings, environment)) {
}
final Path nodesPath = randomFrom(environment.dataFiles()).resolve(NODES_FOLDER);
assertTrue(Files.isDirectory(nodesPath));
IOUtils.rm(nodesPath);

final BiConsumer<byte[], Matcher<String>> testCaseConsumer = (fileContent, causeMatcher) -> {
try {
Files.write(nodesPath, fileContent);
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> new NodeEnvironment(settings, environment));
assertThat(
e.getMessage(),
allOf(containsString("is not compatible"), containsString("perhaps it has already been upgraded to a later version")));
assertThat(e.getCause(), instanceOf(IllegalStateException.class));
assertThat(
e.getCause().getMessage().length(),
lessThanOrEqualTo(nodesPath.toString().length() + "[] is a file which contains [...]".length() + 256));
assertThat(e.getCause().getMessage(), causeMatcher);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};

testCaseConsumer.accept(
"file content which should be reported in the error message".getBytes(StandardCharsets.UTF_8),
endsWith("is a file which contains [file content which should be reported in the error message]")
);

testCaseConsumer.accept(
Stream.iterate("x", x -> x).limit(10000).collect(Collectors.joining()).getBytes(StandardCharsets.UTF_8),
allOf(containsString("is a file which contains [xxxxxx"), endsWith("xxx...]"))
);

final byte[] content = randomByteArrayOfLength(1024);
content[between(0, 255)] = (byte) 0xff; // never valid in a UTF-8 string
testCaseConsumer.accept(content, endsWith("is a file which contains [<unreadable>]"));
}

private void verifyFailsOnShardData(Settings settings, Path indexPath, String shardDataDirName) {
IllegalStateException ex = expectThrows(IllegalStateException.class,
"Must fail creating NodeEnvironment on a data path that has shard data if node does not have data role",
Expand Down

0 comments on commit 0651f94

Please sign in to comment.