Skip to content

Commit bb45c6b

Browse files
authored
Merge pull request #1477 from marklogic/feature/compression-chunking-test
Improved test for HTTP chunking
2 parents 2a32cb5 + fb6f669 commit bb45c6b

File tree

3 files changed

+86
-74
lines changed

3 files changed

+86
-74
lines changed

Jenkinsfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ pipeline{
8383
export PATH=$GRADLE_USER_HOME:$JAVA_HOME/bin:$PATH
8484
cd java-client-api
8585
./gradlew marklogic-client-api:test || true
86-
./gradlew marklogic-client-api-functionaltests:testFastFunctionalTests || true
8786
'''
8887
sh label:'ml development tool test', script: '''#!/bin/bash
8988
export JAVA_HOME=$JAVA_HOME_DIR

marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3986,11 +3986,7 @@ public RESTServiceResultIterator postMultipartForm(
39863986
requestBldr = setupRequest(requestBldr, multiBuilder, null);
39873987
requestBldr = addTransactionScopedCookies(requestBldr, transaction);
39883988
requestBldr = addTelemetryAgentId(requestBldr);
3989-
3990-
if ("rows".equals(path)) {
3991-
requestBldr.addHeader("ML-Check-ML11-Headers", "true");
3992-
requestBldr.addHeader("TE", "trailers");
3993-
}
3989+
requestBldr = addTrailerHeadersIfNecessary(requestBldr, path);
39943990

39953991
Function<Request.Builder, Response> doPostFunction = funcBuilder ->
39963992
doPost(reqlog, funcBuilder.header(HEADER_ACCEPT, multipartMixedWithBoundary()), multiBuilder.build());
@@ -4053,11 +4049,7 @@ private <U extends OkHttpResultIterator> U postIteratedResourceImpl(
40534049
requestBldr = setupRequest(requestBldr, inputMimetype, null);
40544050
requestBldr = addTransactionScopedCookies(requestBldr, transaction);
40554051
requestBldr = addTelemetryAgentId(requestBldr);
4056-
4057-
if ("rows".equals(path)) {
4058-
requestBldr.addHeader("ML-Check-ML11-Headers", "true");
4059-
requestBldr.addHeader("TE", "trailers");
4060-
}
4052+
requestBldr = addTrailerHeadersIfNecessary(requestBldr, path);
40614053

40624054
Consumer<Boolean> resendableConsumer = new Consumer<Boolean>() {
40634055
public void accept(Boolean resendable) {
@@ -4394,6 +4386,17 @@ private Request.Builder addTelemetryAgentId(Request.Builder requestBldr) {
43944386
return requestBldr.header("ML-Agent-ID", "java");
43954387
}
43964388

4389+
private Request.Builder addTrailerHeadersIfNecessary(Request.Builder requestBldr, String path) {
4390+
if ("rows".equals(path)) {
4391+
// Standard header supported by ML 11
4392+
requestBldr.addHeader("TE", "trailers");
4393+
4394+
// Proprietary header recognized by ML 10.0-9, per https://docs.marklogic.com/guide/relnotes/chap3#id_73268
4395+
requestBldr.addHeader("ML-Check-ML11-Headers", "true");
4396+
}
4397+
return requestBldr;
4398+
}
4399+
43974400
private <W extends AbstractWriteHandle> boolean addParts(
43984401
MultipartBody.Builder multiPart, RequestLogger reqlog, W[] input)
43994402
{
@@ -4594,22 +4597,27 @@ private <U extends OkHttpResultIterator> U makeResults(
45944597
}
45954598
List<BodyPart> partList = getPartList(entity);
45964599

4600+
String mlErrorCode = null;
4601+
String mlErrorMessage = null;
45974602
try {
4598-
Headers trailer = response.trailers();
4599-
String code = trailer.get("ml-error-code");
4600-
String msg = trailer.get("ml-error-message");
4601-
String sha = trailer.get("ml-content-sha256");
4602-
4603-
if (code != null && !"N/A".equals(code)) {
4604-
FailedRequest failure = new FailedRequest();
4605-
failure.setMessageString(code);
4606-
failure.setStatusString(msg);
4607-
throw new FailedRequestException("failed to " + operation + " "
4608-
+ entityType + " at rows" + ": " + code + ", " + msg);
4609-
}
4603+
Headers trailers = response.trailers();
4604+
mlErrorCode = trailers.get("ml-error-code");
4605+
mlErrorMessage = trailers.get("ml-error-message");
46104606
} catch (IOException e) {
4611-
throw new RuntimeException("No trailer header in repsonse");
4607+
// This does not seem worthy of causing the entire operation to fail; we also don't expect this to occur, as it
4608+
// should only occur due to a programming error where the response body has already been consumed
4609+
logger.warn("Unexpected IO error while getting HTTP response trailers: " + e.getMessage());
4610+
}
4611+
4612+
if (mlErrorCode != null && !"N/A".equals(mlErrorCode)) {
4613+
FailedRequest failure = new FailedRequest();
4614+
failure.setMessageString(mlErrorCode);
4615+
failure.setStatusString(mlErrorMessage);
4616+
failure.setStatusCode(500);
4617+
throw new FailedRequestException("failed to " + operation + " "
4618+
+ entityType + " at rows" + ": " + mlErrorCode + ", " + mlErrorMessage, failure);
46124619
}
4620+
46134621
Closeable closeable = response;
46144622
return makeResults(constructor, reqlog, operation, entityType, partList, response, closeable);
46154623
}

marklogic-client-api/src/test/java/com/marklogic/client/test/rows/RowManagerTest.java

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,23 @@
1515
*/
1616
package com.marklogic.client.test.rows;
1717

18-
import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
19-
import static org.junit.Assert.assertArrayEquals;
20-
import static org.junit.Assert.assertEquals;
21-
import static org.junit.Assert.assertFalse;
22-
import static org.junit.Assert.assertNotNull;
23-
import static org.junit.Assert.assertTrue;
24-
25-
import java.io.IOException;
26-
import java.io.LineNumberReader;
27-
import java.io.StringWriter;
28-
import java.util.*;
29-
import java.util.stream.Collectors;
30-
31-
import javax.xml.namespace.QName;
32-
import javax.xml.transform.OutputKeys;
33-
import javax.xml.transform.Transformer;
34-
import javax.xml.transform.TransformerConfigurationException;
35-
import javax.xml.transform.TransformerException;
36-
import javax.xml.transform.TransformerFactory;
37-
import javax.xml.transform.TransformerFactoryConfigurationError;
38-
import javax.xml.transform.dom.DOMSource;
39-
import javax.xml.transform.stream.StreamResult;
40-
import javax.xml.xpath.XPathExpressionException;
41-
42-
import com.fasterxml.jackson.databind.node.ObjectNode;
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import com.marklogic.client.FailedRequestException;
4321
import com.marklogic.client.datamovement.DataMovementManager;
4422
import com.marklogic.client.datamovement.WriteBatcher;
23+
import com.marklogic.client.document.DocumentManager;
24+
import com.marklogic.client.expression.PlanBuilder;
4525
import com.marklogic.client.io.*;
4626
import com.marklogic.client.query.DeleteQueryDefinition;
4727
import com.marklogic.client.query.QueryManager;
4828
import com.marklogic.client.row.*;
29+
import com.marklogic.client.row.RowManager.RowSetPart;
30+
import com.marklogic.client.row.RowManager.RowStructure;
31+
import com.marklogic.client.row.RowRecord.ColumnKind;
32+
import com.marklogic.client.test.Common;
4933
import com.marklogic.client.type.*;
34+
import com.marklogic.client.util.EditableNamespaceContext;
5035
import org.junit.AfterClass;
5136
import org.junit.BeforeClass;
5237
import org.junit.Ignore;
@@ -57,15 +42,19 @@
5742
import org.w3c.dom.NodeList;
5843
import org.xml.sax.SAXException;
5944

60-
import com.fasterxml.jackson.databind.JsonNode;
61-
import com.fasterxml.jackson.databind.ObjectMapper;
62-
import com.marklogic.client.document.DocumentManager;
63-
import com.marklogic.client.expression.PlanBuilder;
64-
import com.marklogic.client.row.RowManager.RowSetPart;
65-
import com.marklogic.client.row.RowManager.RowStructure;
66-
import com.marklogic.client.row.RowRecord.ColumnKind;
67-
import com.marklogic.client.test.Common;
68-
import com.marklogic.client.util.EditableNamespaceContext;
45+
import javax.xml.namespace.QName;
46+
import javax.xml.transform.*;
47+
import javax.xml.transform.dom.DOMSource;
48+
import javax.xml.transform.stream.StreamResult;
49+
import javax.xml.xpath.XPathExpressionException;
50+
import java.io.IOException;
51+
import java.io.LineNumberReader;
52+
import java.io.StringWriter;
53+
import java.util.*;
54+
import java.util.stream.Collectors;
55+
56+
import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual;
57+
import static org.junit.Assert.*;
6958

7059
public class RowManagerTest {
7160
private static String[] uris = null;
@@ -528,26 +517,42 @@ public void testSQL0Result() {
528517
}
529518

530519
@Test
531-
public void testSQLException() {
520+
public void testErrorWhileStreamingRows() {
532521
if (!Common.markLogicIsVersion11OrHigher()) {
533522
return;
534523
}
535524

536-
RowManager rowMgr = Common.client.newRowManager();
537-
PlanBuilder p = rowMgr.newPlanBuilder();
538-
PlanBuilder.ExportablePlan builtPlan =
539-
p.fromSql("select case when lastName = 'Davis' then fn_error(fn_qname('', 'SQL-TABLENOTFOUND'), 'Internal Server Error') end, opticUnitTest.musician.* from (select * from opticUnitTest.musician order by lastName)");
540-
String exception = "";
541-
int rowNum = 0;
542-
try {
543-
for (RowRecord row: rowMgr.resultRows(builtPlan)) {
544-
rowNum++;
545-
}
546-
} catch (Exception e) {
547-
exception = e.toString();
548-
}
549-
assertEquals(0, rowNum);
550-
assertEquals("com.marklogic.client.FailedRequestException: failed to apply resource at rows: SQL-TABLENOTFOUND, Internal Server Error", exception);
525+
final String validQueryThatEventuallyThrowsAnError = "select case " +
526+
"when lastName = 'Davis' then fn_error(fn_qname('', 'SQL-TABLENOTFOUND'), 'Internal Server Error') end, " +
527+
"opticUnitTest.musician.* from (select * from opticUnitTest.musician order by lastName)";
528+
529+
RowManager rowManager = Common.client.newRowManager();
530+
PlanBuilder.ModifyPlan plan = rowManager.newPlanBuilder().fromSql(validQueryThatEventuallyThrowsAnError);
531+
532+
FailedRequestException ex = assertThrows(
533+
"The SQL query is designed to not immediately fail - it will immediately return a 200 status code to the " +
534+
"Java Client because the query itself can be executed - but will fail later as it streams rows; " +
535+
"specifically, it will fail on the fourth row, which is the 'Davis' row. " +
536+
"If chunking is configured correctly for the /v1/rows requests - i.e. if the " +
537+
"'TE' header is present - then ML should return trailers in the HTTP response named 'ml-error-code' and " +
538+
"'ml-error-message'. Those are intended to indicate that while a 200 was returned, an error occurred later " +
539+
"while streaming data back. The Java Client is then expected to detect those trailers and throw a " +
540+
"FailedRequestException. If the Java Client does not do that, then no exception will be thrown and this " +
541+
"assertion will fail.", FailedRequestException.class, () -> rowManager.resultRows(plan));
542+
543+
assertEquals("A 500 is expected, even though ML immediately returned a 200 before it started streaming any data " +
544+
"back; a 500 is used instead of a 400 here as we don't have a reliable way of knowing if the error " +
545+
"occurred due to a bad request by the user, since the query was valid in the sense that it could be " +
546+
"executed", 500, ex.getServerStatusCode());
547+
548+
assertEquals("The server error message is expected to be the value of the 'ml-error-message' trailer",
549+
"SQL-TABLENOTFOUND", ex.getServerMessage());
550+
551+
assertEquals(
552+
"The exception message is expected to be a formatted message containing the values of the 'ml-error-code' and " +
553+
"'ml-error-message' trailers",
554+
"Local message: failed to apply resource at rows: SQL-TABLENOTFOUND, Internal Server Error. Server Message: SQL-TABLENOTFOUND",
555+
ex.getMessage());
551556
}
552557

553558
@Test

0 commit comments

Comments
 (0)