From a4a6871db99977d257e10ea29323ccac4557e634 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Oct 2021 20:32:42 +0100 Subject: [PATCH 1/2] Improve error message in 8.x to 7.x downgrade In #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. --- .../elasticsearch/env/NodeEnvironment.java | 37 ++++++++++++ .../env/NodeEnvironmentTests.java | 57 ++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java index 5554521eb222a..c83af604251e1 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java +++ b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java @@ -53,12 +53,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; @@ -252,6 +259,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); @@ -326,6 +344,25 @@ 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 = fileChannel.read(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 ""; + } + } + } + /** * Resolve a specific nodes/{node.id} path for the specified path and node lock id. * diff --git a/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java b/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java index 5b25a4b223e0d..4a134828cee0d 100644 --- a/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java +++ b/server/src/test/java/org/elasticsearch/env/NodeEnvironmentTests.java @@ -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; @@ -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; @@ -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; @@ -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> 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 []")); + } + 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", From 5ad8198e42c695f679f44ab56645daca849a7a87 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Oct 2021 21:20:17 +0100 Subject: [PATCH 2/2] Forbidden --- .../src/main/java/org/elasticsearch/env/NodeEnvironment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java index c83af604251e1..f7fcd91a7a091 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java +++ b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java @@ -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; @@ -346,9 +347,10 @@ 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 = fileChannel.read(byteBuffer); + final int len = Channels.readFromFileChannel(fileChannel, 0, byteBuffer); byteBuffer.flip(); final CharsetDecoder charsetDecoder = StandardCharsets.UTF_8