Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error message in 8.x to 7.x downgrade #78644

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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