16
16
import org .elasticsearch .client .RequestOptions ;
17
17
import org .elasticsearch .client .Response ;
18
18
import org .elasticsearch .client .ResponseException ;
19
+ import org .elasticsearch .client .RestClient ;
19
20
import org .elasticsearch .client .WarningsHandler ;
20
21
import org .elasticsearch .common .bytes .BytesArray ;
21
22
import org .elasticsearch .common .io .Streams ;
43
44
import java .io .InputStream ;
44
45
import java .io .InputStreamReader ;
45
46
import java .io .OutputStream ;
47
+ import java .io .UncheckedIOException ;
46
48
import java .nio .charset .StandardCharsets ;
47
49
import java .time .ZoneId ;
48
50
import java .util .ArrayList ;
53
55
import java .util .Locale ;
54
56
import java .util .Map ;
55
57
import java .util .Set ;
58
+ import java .util .concurrent .ConcurrentHashMap ;
59
+ import java .util .concurrent .ConcurrentMap ;
56
60
import java .util .function .IntFunction ;
57
61
58
62
import static java .util .Collections .emptySet ;
65
69
import static org .elasticsearch .xpack .esql .qa .rest .RestEsqlTestCase .Mode .SYNC ;
66
70
import static org .elasticsearch .xpack .esql .type .EsqlDataTypeConverter .dateTimeToString ;
67
71
import static org .hamcrest .Matchers .any ;
72
+ import static org .hamcrest .Matchers .anyOf ;
68
73
import static org .hamcrest .Matchers .containsString ;
69
74
import static org .hamcrest .Matchers .either ;
70
75
import static org .hamcrest .Matchers .emptyOrNullString ;
@@ -371,7 +376,9 @@ public void testCSVNoHeaderMode() throws IOException {
371
376
options .addHeader ("Content-Type" , mediaType );
372
377
options .addHeader ("Accept" , "text/csv; header=absent" );
373
378
request .setOptions (options );
374
- HttpEntity entity = performRequest (request , new AssertWarnings .NoWarnings ());
379
+ Response response = performRequest (request );
380
+ assertWarnings (response , new AssertWarnings .NoWarnings ());
381
+ HttpEntity entity = response .getEntity ();
375
382
String actual = Streams .copyToString (new InputStreamReader (entity .getContent (), StandardCharsets .UTF_8 ));
376
383
assertEquals ("keyword0,0\r \n " , actual );
377
384
}
@@ -1053,8 +1060,17 @@ static Map<String, Object> runEsql(RequestObjectBuilder requestObject, AssertWar
1053
1060
public static Map <String , Object > runEsqlSync (RequestObjectBuilder requestObject , AssertWarnings assertWarnings ) throws IOException {
1054
1061
Request request = prepareRequestWithOptions (requestObject , SYNC );
1055
1062
1056
- HttpEntity entity = performRequest (request , assertWarnings );
1057
- return entityToMap (entity , requestObject .contentType ());
1063
+ Response response = performRequest (request );
1064
+ HttpEntity entity = response .getEntity ();
1065
+ Map <String , Object > json = entityToMap (entity , requestObject .contentType ());
1066
+
1067
+ var supportsAsyncHeadersFix = hasCapabilities (adminClient (), List .of ("async_query_status_headers_fix" ));
1068
+ if (supportsAsyncHeadersFix ) {
1069
+ assertNoAsyncHeaders (response );
1070
+ }
1071
+ assertWarnings (response , assertWarnings );
1072
+
1073
+ return json ;
1058
1074
}
1059
1075
1060
1076
public static Map <String , Object > runEsqlAsync (RequestObjectBuilder requestObject , AssertWarnings assertWarnings ) throws IOException {
@@ -1082,16 +1098,18 @@ public static Map<String, Object> runEsqlAsync(
1082
1098
checkKeepOnCompletion (requestObject , json , keepOnCompletion );
1083
1099
String id = (String ) json .get ("id" );
1084
1100
1085
- var supportsAsyncHeaders = clusterHasCapability ("POST" , "/_query" , List .of (), List .of ("async_query_status_headers" )).orElse (false );
1101
+ var supportsAsyncHeaders = hasCapabilities (adminClient (), List .of ("async_query_status_headers_fix" ));
1102
+ var supportsSuggestedCast = hasCapabilities (adminClient (), List .of ("suggested_cast" ));
1103
+
1104
+ // Check headers on initial query call
1105
+ if (supportsAsyncHeaders ) {
1106
+ assertAsyncHeaders (response , id , (boolean ) json .get ("is_running" ));
1107
+ }
1086
1108
1087
1109
if (id == null ) {
1088
1110
// no id returned from an async call, must have completed immediately and without keep_on_completion
1089
1111
assertThat (requestObject .keepOnCompletion (), either (nullValue ()).or (is (false )));
1090
1112
assertThat ((boolean ) json .get ("is_running" ), is (false ));
1091
- if (supportsAsyncHeaders ) {
1092
- assertThat (response .getHeader ("X-Elasticsearch-Async-Id" ), nullValue ());
1093
- assertThat (response .getHeader ("X-Elasticsearch-Async-Is-Running" ), is ("?0" ));
1094
- }
1095
1113
assertWarnings (response , assertWarnings );
1096
1114
json .remove ("is_running" ); // remove this to not mess up later map assertions
1097
1115
return Collections .unmodifiableMap (json );
@@ -1112,11 +1130,6 @@ public static Map<String, Object> runEsqlAsync(
1112
1130
assertThat (json .get ("pages" ), nullValue ());
1113
1131
}
1114
1132
1115
- if (supportsAsyncHeaders ) {
1116
- assertThat (response .getHeader ("X-Elasticsearch-Async-Id" ), is (id ));
1117
- assertThat (response .getHeader ("X-Elasticsearch-Async-Is-Running" ), is (isRunning ? "?1" : "?0" ));
1118
- }
1119
-
1120
1133
// issue a second request to "async get" the results
1121
1134
Request getRequest = prepareAsyncGetRequest (id );
1122
1135
getRequest .setOptions (request .getOptions ());
@@ -1126,9 +1139,21 @@ public static Map<String, Object> runEsqlAsync(
1126
1139
1127
1140
var result = entityToMap (entity , requestObject .contentType ());
1128
1141
1142
+ // Check headers on get call
1143
+ if (supportsAsyncHeaders ) {
1144
+ assertAsyncHeaders (response , id , (boolean ) result .get ("is_running" ));
1145
+ }
1146
+
1129
1147
// assert initial contents, if any, are the same as async get contents
1130
1148
if (initialColumns != null ) {
1131
- assertEquals (initialColumns , result .get ("columns" ));
1149
+ if (supportsSuggestedCast == false ) {
1150
+ assertEquals (
1151
+ removeOriginalTypesAndSuggestedCast (initialColumns ),
1152
+ removeOriginalTypesAndSuggestedCast (result .get ("columns" ))
1153
+ );
1154
+ } else {
1155
+ assertEquals (initialColumns , result .get ("columns" ));
1156
+ }
1132
1157
assertEquals (initialValues , result .get ("values" ));
1133
1158
}
1134
1159
@@ -1137,6 +1162,45 @@ public static Map<String, Object> runEsqlAsync(
1137
1162
return removeAsyncProperties (result );
1138
1163
}
1139
1164
1165
+ record CapabilitesCacheKey (RestClient client , List <String > capabilities ) {}
1166
+
1167
+ /**
1168
+ * Cache of capabilities.
1169
+ */
1170
+ private static final ConcurrentMap <CapabilitesCacheKey , Boolean > capabilities = new ConcurrentHashMap <>();
1171
+
1172
+ public static boolean hasCapabilities (RestClient client , List <String > requiredCapabilities ) {
1173
+ if (requiredCapabilities .isEmpty ()) {
1174
+ return true ;
1175
+ }
1176
+ return capabilities .computeIfAbsent (new CapabilitesCacheKey (client , requiredCapabilities ), r -> {
1177
+ try {
1178
+ return clusterHasCapability (client , "POST" , "/_query" , List .of (), requiredCapabilities ).orElse (false );
1179
+ } catch (IOException e ) {
1180
+ throw new UncheckedIOException (e );
1181
+ }
1182
+ });
1183
+ }
1184
+
1185
+ private static Object removeOriginalTypesAndSuggestedCast (Object response ) {
1186
+ if (response instanceof ArrayList <?> columns ) {
1187
+ var newColumns = new ArrayList <>();
1188
+ for (var column : columns ) {
1189
+ if (column instanceof Map <?, ?> columnMap ) {
1190
+ var newMap = new HashMap <>(columnMap );
1191
+ newMap .remove ("original_types" );
1192
+ newMap .remove ("suggested_cast" );
1193
+ newColumns .add (newMap );
1194
+ } else {
1195
+ newColumns .add (column );
1196
+ }
1197
+ }
1198
+ return newColumns ;
1199
+ } else {
1200
+ return response ;
1201
+ }
1202
+ }
1203
+
1140
1204
public void testAsyncGetWithoutContentType () throws IOException {
1141
1205
int count = randomIntBetween (0 , 100 );
1142
1206
bulkLoadTestData (count );
@@ -1278,7 +1342,8 @@ static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String forma
1278
1342
}
1279
1343
1280
1344
Response response = performRequest (request );
1281
- HttpEntity entity = assertWarnings (response , new AssertWarnings .NoWarnings ());
1345
+ assertWarnings (response , new AssertWarnings .NoWarnings ());
1346
+ HttpEntity entity = response .getEntity ();
1282
1347
1283
1348
// get the content, it could be empty because the request might have not completed
1284
1349
String initialValue = Streams .copyToString (new InputStreamReader (entity .getContent (), StandardCharsets .UTF_8 ));
@@ -1331,7 +1396,8 @@ static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String forma
1331
1396
// if `addParam` is false, `options` will already have an `Accept` header
1332
1397
getRequest .setOptions (options );
1333
1398
response = performRequest (getRequest );
1334
- entity = assertWarnings (response , new AssertWarnings .NoWarnings ());
1399
+ assertWarnings (response , new AssertWarnings .NoWarnings ());
1400
+ entity = response .getEntity ();
1335
1401
}
1336
1402
String newValue = Streams .copyToString (new InputStreamReader (entity .getContent (), StandardCharsets .UTF_8 ));
1337
1403
@@ -1345,21 +1411,18 @@ static String runEsqlAsTextWithFormat(RequestObjectBuilder builder, String forma
1345
1411
}
1346
1412
1347
1413
private static Request prepareRequest (Mode mode ) {
1348
- Request request = new Request ("POST" , "/_query" + (mode == ASYNC ? "/async" : "" ));
1349
- request .addParameter ("error_trace" , "true" ); // Helps with debugging in case something crazy happens on the server.
1350
- request .addParameter ("pretty" , "true" ); // Improves error reporting readability
1351
- return request ;
1414
+ return finishRequest (new Request ("POST" , "/_query" + (mode == ASYNC ? "/async" : "" )));
1352
1415
}
1353
1416
1354
1417
private static Request prepareAsyncGetRequest (String id ) {
1355
- Request request = new Request ("GET" , "/_query/async/" + id + "?wait_for_completion_timeout=60s" );
1356
- request .addParameter ("error_trace" , "true" ); // Helps with debugging in case something crazy happens on the server.
1357
- request .addParameter ("pretty" , "true" ); // Improves error reporting readability
1358
- return request ;
1418
+ return finishRequest (new Request ("GET" , "/_query/async/" + id + "?wait_for_completion_timeout=6000s" ));
1359
1419
}
1360
1420
1361
1421
private static Request prepareAsyncDeleteRequest (String id ) {
1362
- Request request = new Request ("DELETE" , "/_query/async/" + id );
1422
+ return finishRequest (new Request ("DELETE" , "/_query/async/" + id ));
1423
+ }
1424
+
1425
+ private static Request finishRequest (Request request ) {
1363
1426
request .addParameter ("error_trace" , "true" ); // Helps with debugging in case something crazy happens on the server.
1364
1427
request .addParameter ("pretty" , "true" ); // Improves error reporting readability
1365
1428
return request ;
@@ -1373,11 +1436,7 @@ private static String attachBody(RequestObjectBuilder requestObject, Request req
1373
1436
return mediaType ;
1374
1437
}
1375
1438
1376
- private static HttpEntity performRequest (Request request , AssertWarnings assertWarnings ) throws IOException {
1377
- return assertWarnings (performRequest (request ), assertWarnings );
1378
- }
1379
-
1380
- private static Response performRequest (Request request ) throws IOException {
1439
+ protected static Response performRequest (Request request ) throws IOException {
1381
1440
Response response = client ().performRequest (request );
1382
1441
if (shouldLog ()) {
1383
1442
LOGGER .info ("RESPONSE={}" , response );
@@ -1387,14 +1446,19 @@ private static Response performRequest(Request request) throws IOException {
1387
1446
return response ;
1388
1447
}
1389
1448
1390
- private static HttpEntity assertWarnings (Response response , AssertWarnings assertWarnings ) {
1449
+ static void assertNotPartial (Map <String , Object > answer ) {
1450
+ var clusters = answer .get ("_clusters" );
1451
+ var reason = "unexpected partial results" + (clusters != null ? ": _clusters=" + clusters : "" );
1452
+ assertThat (reason , answer .get ("is_partial" ), anyOf (nullValue (), is (false )));
1453
+ }
1454
+
1455
+ private static void assertWarnings (Response response , AssertWarnings assertWarnings ) {
1391
1456
List <String > warnings = new ArrayList <>(response .getWarnings ());
1392
1457
warnings .removeAll (mutedWarnings ());
1393
1458
if (shouldLog ()) {
1394
1459
LOGGER .info ("RESPONSE warnings (after muted)={}" , warnings );
1395
1460
}
1396
1461
assertWarnings .assertWarnings (warnings );
1397
- return response .getEntity ();
1398
1462
}
1399
1463
1400
1464
private static Set <String > mutedWarnings () {
@@ -1505,6 +1569,16 @@ private static void createIndex(String indexName, boolean lookupMode, String map
1505
1569
assertEquals (200 , client ().performRequest (request ).getStatusLine ().getStatusCode ());
1506
1570
}
1507
1571
1572
+ private static void assertAsyncHeaders (Response response , @ Nullable String asyncId , boolean isRunning ) {
1573
+ assertThat (response .getHeader ("X-Elasticsearch-Async-Id" ), asyncId == null ? nullValue () : equalTo (asyncId ));
1574
+ assertThat (response .getHeader ("X-Elasticsearch-Async-Is-Running" ), isRunning ? is ("?1" ) : is ("?0" ));
1575
+ }
1576
+
1577
+ private static void assertNoAsyncHeaders (Response response ) {
1578
+ assertThat (response .getHeader ("X-Elasticsearch-Async-Id" ), nullValue ());
1579
+ assertThat (response .getHeader ("X-Elasticsearch-Async-Is-Running" ), nullValue ());
1580
+ }
1581
+
1508
1582
public static RequestObjectBuilder requestObjectBuilder () throws IOException {
1509
1583
return new RequestObjectBuilder ();
1510
1584
}
0 commit comments