[ASTERIXDB-2122][API] Ensure Valid JSON on Result Printing
- user model changes: no
- storage format changes: no
- interface changes: no
Details:
- Ensure results JSON field is closed when an exception is
encountered.
- Always return errors field if it exists in the API response.
- Add test case.
Change-Id: Ic16e9d234c5e127ea24e382c49f3c7cfa5bce9c7
Reviewed-on: https://asterix-gerrit.ics.uci.edu/2057
Sonar-Qube: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Contrib: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Ian Maxon <imaxon@apache.org>
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/result/ResultPrinter.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/result/ResultPrinter.java
index 04ac0b3..c8d25f1 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/result/ResultPrinter.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/result/ResultPrinter.java
@@ -200,32 +200,33 @@
public void print(ResultReader resultReader) throws HyracksDataException {
printPrefix();
+ try {
+ final IFrameTupleAccessor fta = resultReader.getFrameTupleAccessor();
+ final IFrame frame = new VSizeFrame(resultDisplayFrameMgr);
- final IFrameTupleAccessor fta = resultReader.getFrameTupleAccessor();
- final IFrame frame = new VSizeFrame(resultDisplayFrameMgr);
-
- while (resultReader.read(frame) > 0) {
- final ByteBuffer frameBuffer = frame.getBuffer();
- final byte[] frameBytes = frameBuffer.array();
- fta.reset(frameBuffer);
- final int last = fta.getTupleCount();
- for (int tIndex = 0; tIndex < last; tIndex++) {
- final int start = fta.getTupleStartOffset(tIndex);
- int length = fta.getTupleEndOffset(tIndex) - start;
- if (conf.fmt() == SessionConfig.OutputFormat.CSV
- && ((length > 0) && (frameBytes[start + length - 1] == '\n'))) {
- length--;
+ while (resultReader.read(frame) > 0) {
+ final ByteBuffer frameBuffer = frame.getBuffer();
+ final byte[] frameBytes = frameBuffer.array();
+ fta.reset(frameBuffer);
+ final int last = fta.getTupleCount();
+ for (int tIndex = 0; tIndex < last; tIndex++) {
+ final int start = fta.getTupleStartOffset(tIndex);
+ int length = fta.getTupleEndOffset(tIndex) - start;
+ if (conf.fmt() == SessionConfig.OutputFormat.CSV
+ && ((length > 0) && (frameBytes[start + length - 1] == '\n'))) {
+ length--;
+ }
+ String result = new String(frameBytes, start, length, UTF_8);
+ if (wrapArray && notFirst) {
+ output.out().print(", ");
+ }
+ notFirst = true;
+ displayRecord(result);
}
- String result = new String(frameBytes, start, length, UTF_8);
- if (wrapArray && notFirst) {
- output.out().print(", ");
- }
- notFirst = true;
- displayRecord(result);
+ frameBuffer.clear();
}
- frameBuffer.clear();
+ } finally {
+ printPostfix();
}
-
- printPostfix();
}
}
diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/app/result/ResultPrinterTest.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/app/result/ResultPrinterTest.java
new file mode 100644
index 0000000..6810e19
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/app/result/ResultPrinterTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.asterix.app.result;
+
+import static org.apache.hyracks.util.StorageUtil.StorageUnit.KILOBYTE;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.asterix.api.http.server.AbstractQueryApiServlet;
+import org.apache.asterix.api.http.server.ResultUtil;
+import org.apache.asterix.common.api.IApplicationContext;
+import org.apache.asterix.common.config.CompilerProperties;
+import org.apache.asterix.common.exceptions.AsterixException;
+import org.apache.asterix.test.common.ResultExtractor;
+import org.apache.asterix.translator.IStatementExecutor;
+import org.apache.asterix.translator.SessionConfig;
+import org.apache.asterix.translator.SessionOutput;
+import org.apache.commons.io.IOUtils;
+import org.apache.hyracks.util.StorageUtil;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class ResultPrinterTest {
+
+ /**
+ * Ensures that a valid JSON is returned when an exception is encountered
+ * while printing results and that the errors field is returned in the
+ * presence of results fields.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void exceptionWhilePrinting() throws Exception {
+ final RuntimeException expectedException = new RuntimeException("Error reading result");
+ final IApplicationContext appCtx = Mockito.mock(IApplicationContext.class);
+ final CompilerProperties compilerProperties = Mockito.mock(CompilerProperties.class);
+ Mockito.when(appCtx.getCompilerProperties()).thenReturn(compilerProperties);
+ Mockito.when(compilerProperties.getFrameSize()).thenReturn(StorageUtil.getIntSizeInBytes(32, KILOBYTE));
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ final PrintWriter out = new PrintWriter(baos, true);
+ final SessionOutput sessionOutput = createSessionOutput(out);
+ final ResultPrinter rs = new ResultPrinter(appCtx, sessionOutput, new IStatementExecutor.Stats(), null);
+ final ResultReader resultReader = Mockito.mock(ResultReader.class);
+ // throw exception after the resultPrefix is written
+ Mockito.when(resultReader.getFrameTupleAccessor()).thenThrow(new RuntimeException(expectedException));
+ out.print("{");
+ try {
+ rs.print(resultReader);
+ } catch (RuntimeException e) {
+ ResultUtil.printError(out, e, true);
+ printMetrics(out, 1);
+ }
+ out.print("}");
+ out.flush();
+ final String resultStr = new String(baos.toByteArray(), StandardCharsets.UTF_8);
+ boolean exceptionThrown = false;
+ try {
+ // ensure result is valid json and error will be returned and not results.
+ ResultExtractor.extract(IOUtils.toInputStream(resultStr, StandardCharsets.UTF_8));
+ } catch (AsterixException e) {
+ exceptionThrown = true;
+ Assert.assertTrue(e.getMessage().contains(expectedException.getMessage()));
+ }
+ Assert.assertTrue(exceptionThrown);
+ }
+
+ private static SessionOutput createSessionOutput(PrintWriter resultWriter) {
+ SessionOutput.ResultDecorator resultPrefix = ResultUtil.createPreResultDecorator();
+ SessionOutput.ResultDecorator resultPostfix = ResultUtil.createPostResultDecorator();
+ SessionOutput.ResultAppender appendHandle = ResultUtil.createResultHandleAppender(null);
+ SessionOutput.ResultAppender appendStatus = ResultUtil.createResultStatusAppender();
+ SessionConfig.OutputFormat format = SessionConfig.OutputFormat.CLEAN_JSON;
+ SessionConfig sessionConfig = new SessionConfig(format);
+ sessionConfig.set(SessionConfig.FORMAT_WRAPPER_ARRAY, true);
+ sessionConfig.set(SessionConfig.FORMAT_INDENT_JSON, true);
+ sessionConfig.set(SessionConfig.FORMAT_QUOTE_RECORD,
+ format != SessionConfig.OutputFormat.CLEAN_JSON && format != SessionConfig.OutputFormat.LOSSLESS_JSON);
+ return new SessionOutput(sessionConfig, resultWriter, resultPrefix, resultPostfix, appendHandle, appendStatus);
+ }
+
+ private static void printMetrics(PrintWriter pw, long errorCount) {
+ pw.print("\t\"");
+ pw.print(AbstractQueryApiServlet.ResultFields.METRICS.str());
+ pw.print("\": {\n");
+ ResultUtil.printField(pw, "errorCount", errorCount, false);
+ pw.print("\t}\n");
+ }
+}
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/ResultExtractor.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/ResultExtractor.java
index 834c104..b95a283 100644
--- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/ResultExtractor.java
+++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/ResultExtractor.java
@@ -81,11 +81,11 @@
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static InputStream extract(InputStream resultStream) throws Exception {
- return extract(resultStream, EnumSet.of(ResultField.RESULTS, ResultField.ERRORS));
+ return extract(resultStream, EnumSet.of(ResultField.RESULTS));
}
public static InputStream extractMetrics(InputStream resultStream) throws Exception {
- return extract(resultStream, EnumSet.of(ResultField.METRICS, ResultField.ERRORS));
+ return extract(resultStream, EnumSet.of(ResultField.METRICS));
}
public static String extractHandle(InputStream resultStream) throws Exception {
@@ -110,7 +110,8 @@
final ObjectNode result = OBJECT_MAPPER.readValue(resultStr, ObjectNode.class);
LOGGER.fine("+++++++\n" + result + "\n+++++++\n");
-
+ // if we have errors field in the results, we will always return it
+ checkForErrors(result);
final StringBuilder resultBuilder = new StringBuilder();
for (Iterator<String> fieldNameIter = result.fieldNames(); fieldNameIter.hasNext();) {
final String fieldName = fieldNameIter.next();
@@ -154,12 +155,6 @@
}
break;
- case ERRORS:
- final JsonNode errors = fieldValue.get(0).get("msg");
- if (!result.get(ResultField.METRICS.getFieldName()).has("errorCount")) {
- throw new AsterixException("Request reported error but not an errorCount");
- }
- throw new AsterixException(errors.asText());
case REQUEST_ID:
case METRICS:
case CLIENT_CONTEXT_ID:
@@ -174,4 +169,15 @@
}
return IOUtils.toInputStream(resultBuilder.toString(), StandardCharsets.UTF_8);
}
+
+ private static void checkForErrors(ObjectNode result) throws AsterixException {
+ final JsonNode errorsField = result.get(ResultField.ERRORS.getFieldName());
+ if (errorsField != null) {
+ final JsonNode errors = errorsField.get(0).get("msg");
+ if (!result.get(ResultField.METRICS.getFieldName()).has("errorCount")) {
+ throw new AsterixException("Request reported error but not an errorCount");
+ }
+ throw new AsterixException(errors.asText());
+ }
+ }
}
\ No newline at end of file