Fix error handling in SQL's CLI (elastic/x-pack-elasticsearch#2730)

We weren't returning errors correctly from the server
or catching them correctly in the CLI. This fixes that
and adds simple integration tests.

Original commit: elastic/x-pack-elasticsearch@259da0da6f
This commit is contained in:
Nik Everett 2017-10-12 16:32:15 +00:00 committed by GitHub
parent 6478713304
commit 852af7de57
13 changed files with 125 additions and 81 deletions

View File

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.qa.sql.multinode;
import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase;
public class CliErrorsIT extends ErrorsTestCase {
}

View File

@ -5,9 +5,7 @@
*/
package org.elasticsearch.xpack.qa.sql.multinode;
import org.elasticsearch.test.junit.annotations.TestLogging;
import org.elasticsearch.xpack.qa.sql.cli.FetchSizeTestCase;
@TestLogging("org.elasticsearch.xpack.qa.sql.multinode:DEBUG")
public class CliFetchSizeIT extends FetchSizeTestCase {
}

View File

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.qa.sql.nosecurity;
import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase;
public class CliErrorsIT extends ErrorsTestCase {
}

View File

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.qa.sql.security;
import org.apache.lucene.util.LuceneTestCase.AwaitsFix;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.qa.sql.cli.ErrorsTestCase;
@AwaitsFix(bugUrl = "https://github.com/elastic/x-pack-elasticsearch/issues/2074")
public class CliErrorsIT extends ErrorsTestCase {
// NOCOMMIT get this working with security....
@Override
protected Settings restClientSettings() {
return RestSqlIT.securitySettings();
}
}

View File

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.qa.sql.cli;
/**
* Tests for error messages.
*/
public abstract class ErrorsTestCase extends CliIntegrationTestCase {
public void testSelectFromMissingTable() throws Exception {
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT * FROM test"));
assertEquals("line 1:15: Unknown index [test][1;23;31m][0m", readLine());
}
}

View File

@ -9,7 +9,7 @@ import java.sql.Connection;
import java.sql.SQLException;
/**
* Tests for error messages.
* Tests for exceptions and their messages.
*/
public class ErrorsTestCase extends JdbcIntegrationTestCase {
public void testSelectFromMissingTable() throws Exception {

View File

@ -61,7 +61,7 @@ public final class Proto extends AbstractProto {
}
}
enum ResponseType implements AbstractProto.ResponseType {
public enum ResponseType implements AbstractProto.ResponseType {
EXCEPTION(ExceptionResponse::new),
ERROR(ErrorResponse::new),
INFO(InfoResponse::new),

View File

@ -6,10 +6,12 @@
package org.elasticsearch.xpack.sql.cli;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.Proto.ResponseType;
import org.elasticsearch.xpack.sql.net.client.SuppressForbidden;
import org.elasticsearch.xpack.sql.net.client.util.IOUtils;
import org.elasticsearch.xpack.sql.net.client.util.StringUtils;
import org.elasticsearch.xpack.sql.protocol.shared.AbstractQueryInitRequest;
import org.elasticsearch.xpack.sql.protocol.shared.Response;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
@ -287,17 +289,21 @@ public class Cli {
}
private void executeQuery(String line) throws IOException {
QueryResponse response = cliClient.queryInit(line, fetchSize);
Response response = cliClient.queryInit(line, fetchSize);
while (true) {
term.writer().print(ResponseToString.toAnsi(response).toAnsi(term));
term.writer().flush();
if (response.cursor().length == 0) {
if (response.responseType() == ResponseType.ERROR || response.responseType() == ResponseType.EXCEPTION) {
return;
}
QueryResponse queryResponse = (QueryResponse) response;
if (queryResponse.cursor().length == 0) {
return;
}
if (false == fetchSeparator.equals("")) {
term.writer().println(fetchSeparator);
}
response = cliClient.nextPage(response.cursor());
response = cliClient.nextPage(queryResponse.cursor());
}
}

View File

@ -9,9 +9,7 @@ import org.elasticsearch.xpack.sql.cli.net.protocol.InfoRequest;
import org.elasticsearch.xpack.sql.cli.net.protocol.InfoResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.Proto;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryInitRequest;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryInitResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryPageRequest;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryPageResponse;
import org.elasticsearch.xpack.sql.net.client.util.Bytes;
import org.elasticsearch.xpack.sql.protocol.shared.Request;
import org.elasticsearch.xpack.sql.protocol.shared.Response;
@ -31,18 +29,19 @@ public class CliHttpClient implements AutoCloseable {
public InfoResponse serverInfo() {
InfoRequest request = new InfoRequest();
// TODO think about error handling here
return (InfoResponse) sendRequest(request);
}
public QueryInitResponse queryInit(String query, int fetchSize) {
public Response queryInit(String query, int fetchSize) {
// TODO allow customizing the time zone - this is what session set/reset/get should be about
QueryInitRequest request = new QueryInitRequest(query, fetchSize, TimeZone.getTimeZone("UTC"), new TimeoutInfo(0, 0, 0));
return (QueryInitResponse) sendRequest(request);
return sendRequest(request);
}
public QueryPageResponse nextPage(byte[] cursor) {
public Response nextPage(byte[] cursor) {
QueryPageRequest request = new QueryPageRequest(cursor, new TimeoutInfo(0, 0, 0));
return (QueryPageResponse) sendRequest(request);
return sendRequest(request);
}
private Response sendRequest(Request request) {

View File

@ -8,6 +8,7 @@ package org.elasticsearch.xpack.sql.cli;
import org.elasticsearch.xpack.sql.cli.net.protocol.ErrorResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.ExceptionResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.InfoResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.Proto.ResponseType;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryResponse;
import org.elasticsearch.xpack.sql.net.client.SuppressForbidden;
import org.elasticsearch.xpack.sql.protocol.shared.Response;
@ -20,17 +21,19 @@ import java.nio.file.Path;
import static org.jline.utils.AttributedStyle.BOLD;
import static org.jline.utils.AttributedStyle.BRIGHT;
import static org.jline.utils.AttributedStyle.CYAN;
import static org.jline.utils.AttributedStyle.DEFAULT;
import static org.jline.utils.AttributedStyle.RED;
import static org.jline.utils.AttributedStyle.WHITE;
import static org.jline.utils.AttributedStyle.YELLOW;
abstract class ResponseToString {
static AttributedStringBuilder toAnsi(Response response) {
AttributedStringBuilder sb = new AttributedStringBuilder();
if (response instanceof QueryResponse) {
switch ((ResponseType) response.responseType()) {
case QUERY_INIT:
case QUERY_PAGE:
QueryResponse cmd = (QueryResponse) response;
if (cmd.data != null) {
String data = cmd.data.toString();
@ -41,12 +44,8 @@ abstract class ResponseToString {
sb.append(data, DEFAULT.foreground(WHITE));
}
}
}
else if (response instanceof ExceptionResponse) {
ExceptionResponse ex = (ExceptionResponse) response;
sb.append(ex.message, BOLD.foreground(CYAN));
}
else if (response instanceof InfoResponse) {
return sb;
case INFO:
InfoResponse info = (InfoResponse) response;
sb.append("Node:", DEFAULT.foreground(BRIGHT));
sb.append(info.node, DEFAULT.foreground(WHITE));
@ -54,17 +53,24 @@ abstract class ResponseToString {
sb.append(info.cluster, DEFAULT.foreground(WHITE));
sb.append(" Version:", DEFAULT.foreground(BRIGHT));
sb.append(info.versionString, DEFAULT.foreground(WHITE));
return sb;
case ERROR:
ErrorResponse err = (ErrorResponse) response;
error("Server error", err.message, sb);
return sb;
case EXCEPTION:
ExceptionResponse ex = (ExceptionResponse) response;
error("Bad request", ex.message, sb);
return sb;
default:
throw new IllegalArgumentException("Unsupported response: " + response);
}
else if (response instanceof ErrorResponse) {
ErrorResponse error = (ErrorResponse) response;
sb.append("Server error:", BOLD.foreground(RED));
sb.append(error.message, DEFAULT.italic().foreground(RED));
}
else {
sb.append("Invalid response received from server...", BOLD.foreground(RED));
}
}
return sb;
private static void error(String type, String message, AttributedStringBuilder sb) {
sb.append(type + " [", BOLD.foreground(RED));
sb.append(message, DEFAULT.boldOff().italic().foreground(YELLOW));
sb.append("]", BOLD.underlineOff().foreground(RED));
}
// NOCOMMIT - is using the default temp folder a problem?

View File

@ -33,8 +33,8 @@ public class ResponseToStringTests extends ESTestCase {
public void testExceptionResponse() {
AttributedStringBuilder s = ResponseToString.toAnsi(new ExceptionResponse(RequestType.INFO, "test message", "test cause",
randomFrom(SqlExceptionType.values())));
assertEquals("test message", unstyled(s));
assertEquals("[1;36mtest message[0m", fullyStyled(s));
assertEquals("Bad request [test message]", unstyled(s));
assertEquals("[1;31mBad request [[22;3;33mtest message[1;23;31m][0m", fullyStyled(s));
}
private String unstyled(AttributedStringBuilder s) {

View File

@ -6,65 +6,67 @@
package org.elasticsearch.xpack.sql.plugin;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.main.MainAction;
import org.elasticsearch.action.main.MainRequest;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestChannel;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestActionListener;
import org.elasticsearch.xpack.sql.cli.net.protocol.InfoResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.Proto;
import org.elasticsearch.xpack.sql.cli.net.protocol.Proto.RequestType;
import org.elasticsearch.xpack.sql.cli.net.protocol.ErrorResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.ExceptionResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryInitRequest;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryInitResponse;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryPageRequest;
import org.elasticsearch.xpack.sql.cli.net.protocol.QueryPageResponse;
import org.elasticsearch.xpack.sql.plugin.sql.action.SqlAction;
import org.elasticsearch.xpack.sql.plugin.sql.action.SqlRequest;
import org.elasticsearch.xpack.sql.protocol.shared.AbstractProto.SqlExceptionType;
import org.elasticsearch.xpack.sql.protocol.shared.Request;
import org.elasticsearch.xpack.sql.protocol.shared.Response;
import org.elasticsearch.xpack.sql.session.Cursor;
import org.joda.time.DateTimeZone;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.elasticsearch.rest.BytesRestResponse.TEXT_CONTENT_TYPE;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestStatus.OK;
public class RestSqlCliAction extends BaseRestHandler {
public class RestSqlCliAction extends AbstractSqlProtocolRestAction {
private final NamedWriteableRegistry cursorRegistry = new NamedWriteableRegistry(Cursor.getNamedWriteables());
public RestSqlCliAction(Settings settings, RestController controller) {
super(settings);
super(settings, Proto.INSTANCE);
controller.registerHandler(POST, "/_sql/cli", this);
}
@Override
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
Request request;
try (DataInputStream in = new DataInputStream(restRequest.content().streamInput())) {
request = Proto.INSTANCE.readRequest(in);
}
public String getName() {
return "xpack_sql_cli_action";
}
@Override
protected RestChannelConsumer innerPrepareRequest(Request request, Client client) throws IOException {
Consumer<RestChannel> consumer = operation(request, client);
return consumer::accept;
}
@Override
protected ErrorResponse buildErrorResponse(Request request, String message, String cause, String stack) {
return new ErrorResponse((RequestType) request.requestType(), message, cause, stack);
}
@Override
protected ExceptionResponse buildExceptionResponse(Request request, String message, String cause, SqlExceptionType exceptionType) {
return new ExceptionResponse((RequestType) request.requestType(), message, cause, exceptionType);
}
/**
* Actual implementation of the operation
*/
@ -73,7 +75,7 @@ public class RestSqlCliAction extends BaseRestHandler {
RequestType requestType = (RequestType) request.requestType();
switch (requestType) {
case INFO:
return channel -> client.execute(MainAction.INSTANCE, new MainRequest(), toActionListener(channel, response ->
return channel -> client.execute(MainAction.INSTANCE, new MainRequest(), toActionListener(request, channel, response ->
new InfoResponse(response.getNodeName(), response.getClusterName().value(),
response.getVersion().major, response.getVersion().minor, response.getVersion().toString(),
response.getBuild().shortHash(), response.getBuild().date())));
@ -86,11 +88,11 @@ public class RestSqlCliAction extends BaseRestHandler {
}
}
private static Consumer<RestChannel> queryInit(Client client, QueryInitRequest request) {
private Consumer<RestChannel> queryInit(Client client, QueryInitRequest request) {
// TODO time zone support for CLI
SqlRequest sqlRequest = new SqlRequest(request.query, DateTimeZone.forTimeZone(request.timeZone), request.fetchSize, Cursor.EMPTY);
long start = System.nanoTime();
return channel -> client.execute(SqlAction.INSTANCE, sqlRequest, toActionListener(channel, response -> {
return channel -> client.execute(SqlAction.INSTANCE, sqlRequest, toActionListener(request, channel, response -> {
CliFormatter formatter = new CliFormatter(response);
String data = formatter.formatWithHeader(response);
return new QueryInitResponse(System.nanoTime() - start, serializeCursor(response.cursor(), formatter), data);
@ -108,7 +110,7 @@ public class RestSqlCliAction extends BaseRestHandler {
}
SqlRequest sqlRequest = new SqlRequest("", SqlRequest.DEFAULT_TIME_ZONE, -1, cursor);
long start = System.nanoTime();
return channel -> client.execute(SqlAction.INSTANCE, sqlRequest, toActionListener(channel, response -> {
return channel -> client.execute(SqlAction.INSTANCE, sqlRequest, toActionListener(request, channel, response -> {
String data = formatter.formatWithoutHeader(response);
return new QueryPageResponse(System.nanoTime() - start, serializeCursor(response.cursor(), formatter), data);
}));
@ -126,26 +128,4 @@ public class RestSqlCliAction extends BaseRestHandler {
throw new RuntimeException("unexpected trouble building the cursor", e);
}
}
private static <T> ActionListener<T> toActionListener(RestChannel channel, Function<T, Response> responseBuilder) {
// NOCOMMIT error response
return new RestActionListener<T>(channel) {
@Override
protected void processResponse(T actionResponse) throws Exception {
Response response = responseBuilder.apply(actionResponse);
try (BytesStreamOutput bytesStreamOutput = new BytesStreamOutput()) {
try (DataOutputStream dataOutputStream = new DataOutputStream(bytesStreamOutput)) {
// NOCOMMIT use the version from the client
Proto.INSTANCE.writeResponse(response, Proto.CURRENT_VERSION, dataOutputStream);
}
channel.sendResponse(new BytesRestResponse(OK, TEXT_CONTENT_TYPE, bytesStreamOutput.bytes()));
}
}
};
}
@Override
public String getName() {
return "xpack_sql_cli_action";
}
}

View File

@ -37,7 +37,6 @@ import org.elasticsearch.xpack.sql.jdbc.net.protocol.QueryPageResponse;
import org.elasticsearch.xpack.sql.plugin.sql.action.SqlAction;
import org.elasticsearch.xpack.sql.plugin.sql.action.SqlRequest;
import org.elasticsearch.xpack.sql.plugin.sql.action.SqlResponse;
import org.elasticsearch.xpack.sql.protocol.shared.AbstractExceptionResponse;
import org.elasticsearch.xpack.sql.protocol.shared.AbstractProto.SqlExceptionType;
import org.elasticsearch.xpack.sql.protocol.shared.Request;
import org.elasticsearch.xpack.sql.session.Cursor;
@ -85,8 +84,7 @@ public class RestSqlJdbcAction extends AbstractSqlProtocolRestAction {
}
@Override
protected AbstractExceptionResponse buildExceptionResponse(Request request, String message, String cause,
SqlExceptionType exceptionType) {
protected ExceptionResponse buildExceptionResponse(Request request, String message, String cause, SqlExceptionType exceptionType) {
return new ExceptionResponse((RequestType) request.requestType(), message, cause, exceptionType);
}