Browse Source

EQL: Minimise CCS roundtrips (#76076)

This introduces an optimisation of the EQL requests when these target
one remote cluster only (i.e. no mixed local and remote indices or
multiple remote clusters). In this case, the EQL request is forwarded
to the remote cluster and executed there, instead of having the local
cluster perform multiple queries to the remote cluster.
Bogdan Pintea 4 years ago
parent
commit
7a5ac3e4a9

+ 10 - 0
docs/reference/eql/eql-search-api.asciidoc

@@ -89,6 +89,16 @@ request that targets only `bar*` still returns an error.
 +
 Defaults to `true`.
 
+`ccs_minimize_roundtrips`::
+(Optional, Boolean) If `true`, network round-trips between the local and the
+remote cluster are minimized when running cross-cluster search (CCS) requests.
++
+This option is effective for requests that target data fully contained in one
+remote cluster; when data is spread across multiple clusters, the setting is
+ignored.
++
+Defaults to `true`.
+
 
 include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=expand-wildcards]
 +

+ 6 - 1
x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/BaseEqlSpecTestCase.java

@@ -132,7 +132,12 @@ public abstract class BaseEqlSpecTestCase extends RemoteClusterAwareEqlRestTestC
             .setConnectTimeout(timeout)
             .setSocketTimeout(timeout)
             .build();
-        return eqlClient.search(request, RequestOptions.DEFAULT.toBuilder().setRequestConfig(config).build());
+        RequestOptions.Builder optionsBuilder = RequestOptions.DEFAULT.toBuilder();
+        Boolean ccsMinimizeRoundtrips = ccsMinimizeRoundtrips();
+        if (ccsMinimizeRoundtrips != null) {
+            optionsBuilder.addParameter("ccs_minimize_roundtrips", ccsMinimizeRoundtrips.toString());
+        }
+        return eqlClient.search(request, optionsBuilder.setRequestConfig(config).build());
     }
 
     protected EqlClient eqlClient() {

+ 4 - 0
x-pack/plugin/eql/qa/common/src/main/java/org/elasticsearch/test/eql/RemoteClusterAwareEqlRestTestCase.java

@@ -93,6 +93,10 @@ public abstract class RemoteClusterAwareEqlRestTestCase extends ESRestTestCase {
         return remoteClient == null ? client() : remoteClient;
     }
 
+    protected Boolean ccsMinimizeRoundtrips() {
+        return remoteClient == null ? null : randomBoolean();
+    }
+
     protected static RestClient provisioningAdminClient() {
         return remoteClient == null ? adminClient() : remoteClient;
     }

+ 9 - 11
x-pack/plugin/eql/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/eql/EqlRestValidationIT.java

@@ -17,21 +17,19 @@ public class EqlRestValidationIT extends EqlRestValidationTestCase {
 
     @Override
     protected String getInexistentIndexErrorMessage() {
-        return "\"caused_by\":{\"type\":\"verification_exception\",\"reason\":\"Found 1 problem\\nline -1:-1: Unknown index ";
+        return "\"root_cause\":[{\"type\":\"index_not_found_exception\",\"reason\":\"no such index ";
     }
 
     protected void assertErrorMessageWhenAllowNoIndicesIsFalse(String reqParameter) throws IOException {
-        assertErrorMessage("inexistent1*", reqParameter, "\"root_cause\":[{\"type\":\"index_not_found_exception\","
-            + "\"reason\":\"no such index [inexistent1*]\"");
-        assertErrorMessage("inexistent1*,inexistent2*", reqParameter, "\"root_cause\":[{\"type\":\"index_not_found_exception\","
-            + "\"reason\":\"no such index [inexistent1*]\"");
-        assertErrorMessage("test_eql,inexistent*", reqParameter, "\"root_cause\":[{\"type\":\"index_not_found_exception\","
-            + "\"reason\":\"no such index [inexistent*]\"");
+        assertErrorMessage("inexistent1*", reqParameter, getInexistentIndexErrorMessage() + "[" + indexPattern("inexistent1*") + "]\"");
+        assertErrorMessage("inexistent1*,inexistent2*", reqParameter, getInexistentIndexErrorMessage() +
+            "[" + indexPattern("inexistent1*") + "]\"");
+        assertErrorMessage("test_eql,inexistent*", reqParameter, getInexistentIndexErrorMessage() +
+            "[" + indexPattern("inexistent*") + "]\"");
         //TODO: revisit the next two tests when https://github.com/elastic/elasticsearch/issues/64190 is closed
-        assertErrorMessage("inexistent", reqParameter, "\"root_cause\":[{\"type\":\"index_not_found_exception\","
-            + "\"reason\":\"no such index [[inexistent]]\"");
-        assertErrorMessage("inexistent1,inexistent2", reqParameter, "\"root_cause\":[{\"type\":\"index_not_found_exception\","
-            + "\"reason\":\"no such index [[inexistent1, inexistent2]]\"");
+        assertErrorMessage("inexistent", reqParameter, getInexistentIndexErrorMessage() + "[" + indexPattern("inexistent") + "]\"");
+        assertErrorMessage("inexistent1,inexistent2", reqParameter, getInexistentIndexErrorMessage() +
+            "[" + indexPattern("inexistent1") + "," + indexPattern("inexistent2") + "]\"");
     }
 
     @Override

+ 5 - 4
x-pack/plugin/eql/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/eql/RemoteClusterTestUtils.java

@@ -7,6 +7,8 @@
 
 package org.elasticsearch.xpack.eql;
 
+import java.util.StringJoiner;
+
 public class RemoteClusterTestUtils {
     public static final String REMOTE_CLUSTER_NAME = "my_remote_cluster"; // gradle defined
 
@@ -15,11 +17,10 @@ public class RemoteClusterTestUtils {
     }
 
     public static String remoteClusterPattern(String pattern) {
-        StringBuilder sb = new StringBuilder();
+        StringJoiner sj = new StringJoiner(",");
         for (String index: pattern.split(",")) {
-            sb.append(remoteClusterIndex(index));
-            sb.append(',');
+            sj.add(remoteClusterIndex(index));
         }
-        return sb.substring(0, sb.length() - 1);
+        return sj.toString();
     }
 }

+ 18 - 0
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchRequest.java

@@ -57,6 +57,7 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re
     private int size = RequestDefaults.SIZE;
     private int fetchSize = RequestDefaults.FETCH_SIZE;
     private String query;
+    private boolean ccsMinimizeRoundtrips = RequestDefaults.CCS_MINIMIZE_ROUNDTRIPS;
     private String resultPosition = "tail";
     private List<FieldAndFormat> fetchFields;
     private Map<String, Object> runtimeMappings = emptyMap();
@@ -110,6 +111,9 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re
         size = in.readVInt();
         fetchSize = in.readVInt();
         query = in.readString();
+        if (in.getVersion().onOrAfter(Version.V_7_15_0)) {
+            this.ccsMinimizeRoundtrips = in.readBoolean();
+        }
         if (in.getVersion().onOrAfter(Version.V_8_0_0)) { // TODO: Remove after backport
             this.waitForCompletionTimeout = in.readOptionalTimeValue();
             this.keepAlive = in.readOptionalTimeValue();
@@ -348,6 +352,15 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re
         return this;
     }
 
+    public EqlSearchRequest ccsMinimizeRoundtrips(boolean ccsMinimizeRoundtrips) {
+        this.ccsMinimizeRoundtrips = ccsMinimizeRoundtrips;
+        return this;
+    }
+
+    public boolean ccsMinimizeRoundtrips() {
+        return ccsMinimizeRoundtrips;
+    }
+
     public String resultPosition() {
         return resultPosition;
     }
@@ -394,6 +407,9 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re
         out.writeVInt(size);
         out.writeVInt(fetchSize);
         out.writeString(query);
+        if (out.getVersion().onOrAfter(Version.V_7_15_0)) {
+            out.writeBoolean(ccsMinimizeRoundtrips);
+        }
         if (out.getVersion().onOrAfter(Version.V_8_0_0)) { // TODO: Remove after backport
             out.writeOptionalTimeValue(waitForCompletionTimeout);
             out.writeOptionalTimeValue(keepAlive);
@@ -430,6 +446,7 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re
                 Objects.equals(tiebreakerField, that.tiebreakerField) &&
                 Objects.equals(eventCategoryField, that.eventCategoryField) &&
                 Objects.equals(query, that.query) &&
+                Objects.equals(ccsMinimizeRoundtrips, that.ccsMinimizeRoundtrips) &&
                 Objects.equals(waitForCompletionTimeout, that.waitForCompletionTimeout) &&
                 Objects.equals(keepAlive, that.keepAlive) &&
                 Objects.equals(resultPosition, that.resultPosition) &&
@@ -450,6 +467,7 @@ public class EqlSearchRequest extends ActionRequest implements IndicesRequest.Re
             tiebreakerField,
             eventCategoryField,
             query,
+            ccsMinimizeRoundtrips,
             waitForCompletionTimeout,
             keepAlive,
             resultPosition,

+ 5 - 1
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/EqlSearchResponse.java

@@ -230,7 +230,7 @@ public class EqlSearchResponse extends ActionResponse implements ToXContentObjec
             }, FIELDS);
         }
 
-        private final String index;
+        private String index;
         private final String id;
         private final BytesReference source;
         private final Map<String, DocumentField> fetchFields;
@@ -292,6 +292,10 @@ public class EqlSearchResponse extends ActionResponse implements ToXContentObjec
             return PARSER.apply(parser, null);
         }
 
+        public void index(String index) {
+            this.index = index;
+        }
+
         public String index() {
             return index;
         }

+ 1 - 0
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/action/RequestDefaults.java

@@ -16,4 +16,5 @@ public final class RequestDefaults {
 
     public static int SIZE = 10;
     public static int FETCH_SIZE = 1000;
+    public static boolean CCS_MINIMIZE_ROUNDTRIPS = true;
 }

+ 1 - 0
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java

@@ -62,6 +62,7 @@ public class RestEqlSearchAction extends BaseRestHandler {
                 eqlRequest.keepAlive(request.paramAsTime("keep_alive", eqlRequest.keepAlive()));
             }
             eqlRequest.keepOnCompletion(request.paramAsBoolean("keep_on_completion", eqlRequest.keepOnCompletion()));
+            eqlRequest.ccsMinimizeRoundtrips(request.paramAsBoolean("ccs_minimize_roundtrips", eqlRequest.ccsMinimizeRoundtrips()));
         }
 
         return channel -> {

+ 116 - 16
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/TransportEqlSearchAction.java

@@ -21,11 +21,15 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.time.DateUtils;
 import org.elasticsearch.common.util.BigArrays;
 import org.elasticsearch.core.TimeValue;
+import org.elasticsearch.index.IndexNotFoundException;
 import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.search.fetch.subphase.FieldAndFormat;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.tasks.TaskId;
 import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.RemoteClusterAware;
+import org.elasticsearch.transport.RemoteTransportException;
+import org.elasticsearch.transport.TransportRequestOptions;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.XPackPlugin;
 import org.elasticsearch.xpack.core.XPackSettings;
@@ -47,8 +51,11 @@ import java.io.IOException;
 import java.time.ZoneId;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
 
 import static org.elasticsearch.action.ActionListener.wrap;
+import static org.elasticsearch.transport.RemoteClusterAware.buildRemoteIndexName;
 import static org.elasticsearch.xpack.core.ClientHelper.ASYNC_SEARCH_ORIGIN;
 import static org.elasticsearch.xpack.ql.plugin.TransportActionUtils.executeRequestWithRetryAttempt;
 
@@ -105,7 +112,7 @@ public class TransportEqlSearchAction extends HandledTransportAction<EqlSearchRe
 
     @Override
     protected void doExecute(Task task, EqlSearchRequest request, ActionListener<EqlSearchResponse> listener) {
-        if (request.waitForCompletionTimeout() != null && request.waitForCompletionTimeout().getMillis() >= 0) {
+        if (requestIsAsync(request)) {
             asyncTaskManagementService.asyncExecute(request, request.waitForCompletionTimeout(), request.keepAlive(),
                 request.keepOnCompletion(), listener);
         } else {
@@ -125,25 +132,39 @@ public class TransportEqlSearchAction extends HandledTransportAction<EqlSearchRe
         TimeValue timeout = TimeValue.timeValueSeconds(30);
         String clientId = null;
 
-        ParserParams params = new ParserParams(zoneId)
-            .fieldEventCategory(request.eventCategoryField())
-            .fieldTimestamp(request.timestampField())
-            .fieldTiebreaker(request.tiebreakerField())
-            .resultPosition("tail".equals(request.resultPosition()) ? Order.OrderDirection.DESC : Order.OrderDirection.ASC)
-            .size(request.size())
-            .fetchSize(request.fetchSize());
-
         RemoteClusterRegistry remoteClusterRegistry = new RemoteClusterRegistry(transportService.getRemoteClusterService(),
             request.indicesOptions());
-        EqlConfiguration cfg = new EqlConfiguration(request.indices(), zoneId, username, clusterName, filter,
+        Set<String> clusterAliases = remoteClusterRegistry.clusterAliases(request.indices(), false);
+        if (canMinimizeRountrips(request, clusterAliases)) {
+            String clusterAlias = clusterAliases.iterator().next();
+            String[] remoteIndices = new String[request.indices().length];
+            for (int i = 0; i < request.indices().length; i++) {
+                remoteIndices[i] = request.indices()[i].substring(clusterAlias.length() + 1); // strip cluster plus `:` delimiter
+            }
+            transportService.sendRequest(transportService.getRemoteClusterService().getConnection(clusterAlias),
+                EqlSearchAction.INSTANCE.name(), request.indices(remoteIndices), TransportRequestOptions.EMPTY,
+                new ActionListenerResponseHandler<>(wrap(r -> listener.onResponse(qualifyHits(r, clusterAlias)),
+                    e -> listener.onFailure(qualifyException(e, remoteIndices, clusterAlias))),
+                    EqlSearchAction.INSTANCE.getResponseReader()));
+        } else {
+            ParserParams params = new ParserParams(zoneId)
+                .fieldEventCategory(request.eventCategoryField())
+                .fieldTimestamp(request.timestampField())
+                .fieldTiebreaker(request.tiebreakerField())
+                .resultPosition("tail".equals(request.resultPosition()) ? Order.OrderDirection.DESC : Order.OrderDirection.ASC)
+                .size(request.size())
+                .fetchSize(request.fetchSize());
+
+            EqlConfiguration cfg = new EqlConfiguration(request.indices(), zoneId, username, clusterName, filter,
                 request.runtimeMappings(), fetchFields, timeout, request.indicesOptions(), request.fetchSize(),
                 clientId, new TaskId(nodeId, task.getId()), task, remoteClusterRegistry::versionIncompatibleClusters);
-        executeRequestWithRetryAttempt(clusterService, listener::onFailure,
-            onFailure -> planExecutor.eql(cfg, request.query(), params,
-                wrap(r -> listener.onResponse(createResponse(r, task.getExecutionId())), onFailure)),
-            node -> transportService.sendRequest(node, EqlSearchAction.NAME, request,
-                new ActionListenerResponseHandler<>(listener, EqlSearchResponse::new, ThreadPool.Names.SAME)),
-            log);
+            executeRequestWithRetryAttempt(clusterService, listener::onFailure,
+                onFailure -> planExecutor.eql(cfg, request.query(), params,
+                    wrap(r -> listener.onResponse(createResponse(r, task.getExecutionId())), onFailure)),
+                node -> transportService.sendRequest(node, EqlSearchAction.NAME, request,
+                    new ActionListenerResponseHandler<>(listener, EqlSearchResponse::new, ThreadPool.Names.SAME)),
+                log);
+        }
     }
 
     static EqlSearchResponse createResponse(Results results, AsyncExecutionId id) {
@@ -155,6 +176,85 @@ public class TransportEqlSearchAction extends HandledTransportAction<EqlSearchRe
         }
     }
 
+    private static boolean requestIsAsync(EqlSearchRequest request) {
+        return request.waitForCompletionTimeout() != null && request.waitForCompletionTimeout().getMillis() >= 0;
+    }
+
+    // can the request be proxied to the remote cluster?
+    private static boolean canMinimizeRountrips(EqlSearchRequest request, Set<String> clusterAliases) {
+        // Has minimizing the round trips been (explicitly) disabled?
+        if (request.ccsMinimizeRoundtrips() == false) {
+            return false;
+        }
+        // Is this a search against a single, remote cluster?
+        if (clusterAliases.size() != 1 || clusterAliases.contains(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) {
+            return false;
+        }
+        // The remote async ID would not be valid on local cluster; furthermore on results fetching we would know neither if the ID is
+        // remote or not, nor which remote cluster it belongs to (TODO: could rewrite the ID to smth like [alias:ID])
+        return requestIsAsync(request) == false;
+    }
+
+    // fixes the _index values by prefixing them with the source cluster alias' name
+    private static EqlSearchResponse qualifyHits(EqlSearchResponse r, String clusterAlias) {
+        EqlSearchResponse.Hits hits = r.hits();
+        if (hits.sequences() != null) {
+            for (EqlSearchResponse.Sequence s : hits.sequences()) {
+                qualifyEvents(s.events(), clusterAlias);
+            }
+        } else {
+            qualifyEvents(hits.events(), clusterAlias);
+        }
+        return r;
+    }
+
+    private static void qualifyEvents(List<EqlSearchResponse.Event> events, String clusterAlias) {
+        if (events != null) {
+            for (EqlSearchResponse.Event e : events) {
+                e.index(buildRemoteIndexName(clusterAlias, e.index()));
+            }
+        }
+    }
+
+    private static Exception qualifyException(Exception e, String[] indices, String clusterAlias) {
+        Exception finalException = e;
+        if (e instanceof RemoteTransportException && e.getCause() instanceof IndexNotFoundException) {
+            IndexNotFoundException infe = (IndexNotFoundException) e.getCause();
+            if (infe.getIndex() != null) {
+                String qualifiedIndex;
+                String exceptionIndexName = infe.getIndex().getName();
+                String[] notFoundIndices = notFoundIndices(exceptionIndexName, indices);
+                if (notFoundIndices != null) {
+                    StringJoiner sj = new StringJoiner(",");
+                    for (String notFoundIndex : notFoundIndices) {
+                        sj.add(buildRemoteIndexName(clusterAlias, notFoundIndex));
+                    }
+                    qualifiedIndex = sj.toString();
+                } else {
+                    qualifiedIndex = buildRemoteIndexName(clusterAlias, exceptionIndexName);
+                }
+                // This will expose a "uniform" failure root_cause, with same "type" ("index_not_found_exception") and "reason" ("no such
+                // index [...]"); this is also similar to a non-CCS `POST inexistent/_eql/search?ignore_unavailable=false`, but
+                // unfortunately unlike an inexistent pattern search: `POST inexistent*/_eql/search?ignore_unavailable=false, which raises a
+                // VerificationException as it's root cause. I.e. the failures are not homogenous.
+                finalException = new RemoteTransportException(e.getMessage(), new IndexNotFoundException(qualifiedIndex));
+            }
+        }
+        return finalException;
+    }
+
+    private static String[] notFoundIndices(String exceptionIndexName, String[] indices) {
+        final String[] EXCEPTION_PREFIXES = new String[] {"Unknown index [", "["};
+        for (String prefix : EXCEPTION_PREFIXES) {
+            if (exceptionIndexName.startsWith(prefix) && exceptionIndexName.endsWith("]")) {
+                String indexList = exceptionIndexName.substring(prefix.length(), exceptionIndexName.length() - 1);
+                // see RestEqlSearchAction#prepareRequest() or GH#63529 for an explanation of "*,-*" replacement
+                return indexList.equals("*,-*") ? indices : indexList.split(",[ ]?");
+            }
+        }
+        return null;
+    }
+
     static String username(SecurityContext securityContext) {
         return securityContext != null && securityContext.getUser() != null ? securityContext.getUser().principal() : null;
     }

+ 7 - 8
x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/util/RemoteClusterRegistry.java

@@ -8,13 +8,11 @@
 package org.elasticsearch.xpack.eql.util;
 
 import org.elasticsearch.Version;
-import org.elasticsearch.action.OriginalIndices;
 import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.transport.RemoteClusterAware;
 import org.elasticsearch.transport.RemoteClusterService;
 
-import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -30,7 +28,7 @@ public class RemoteClusterRegistry {
 
     public Set<String> versionIncompatibleClusters(String indexPattern) {
         Set<String> incompatibleClusters = new TreeSet<>();
-        for (String clusterAlias: indicesPerRemoteCluster(indexPattern).keySet()) {
+        for (String clusterAlias: clusterAliases(Strings.splitStringByCommaToArray(indexPattern), true)) {
             Version clusterVersion = remoteClusterService.getConnection(clusterAlias).getVersion();
             if (clusterVersion.equals(Version.CURRENT) == false) { // TODO: should newer clusters be eventually allowed?
                 incompatibleClusters.add(clusterAlias);
@@ -39,10 +37,11 @@ public class RemoteClusterRegistry {
         return incompatibleClusters;
     }
 
-    private Map<String, OriginalIndices> indicesPerRemoteCluster(String indexPattern) {
-        Map<String, OriginalIndices> indicesMap = remoteClusterService.groupIndices(indicesOptions,
-            Strings.splitStringByCommaToArray(indexPattern));
-        indicesMap.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
-        return indicesMap;
+    public Set<String> clusterAliases(String[] indices, boolean discardLocal) {
+        Set<String> clusters = remoteClusterService.groupIndices(indicesOptions, indices).keySet();
+        if (discardLocal) {
+            clusters.remove(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY);
+        }
+        return clusters;
     }
 }

+ 5 - 1
x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/action/EqlSearchRequestTests.java

@@ -38,6 +38,7 @@ public class EqlSearchRequestTests extends AbstractBWCSerializationTestCase<EqlS
         "}";
 
     static String defaultTestIndex = "endgame-*";
+    boolean ccsMinimizeRoundtrips;
 
     @Before
     public void setup() {
@@ -67,6 +68,7 @@ public class EqlSearchRequestTests extends AbstractBWCSerializationTestCase<EqlS
                 randomFetchFields = null;
             }
             QueryBuilder filter = parseFilter(defaultTestFilter);
+            ccsMinimizeRoundtrips = randomBoolean();
             return new EqlSearchRequest()
                 .indices(defaultTestIndex)
                 .filter(filter)
@@ -75,6 +77,7 @@ public class EqlSearchRequestTests extends AbstractBWCSerializationTestCase<EqlS
                 .fetchSize(randomIntBetween(1, 50))
                 .size(randomInt(50))
                 .query(randomAlphaOfLength(10))
+                .ccsMinimizeRoundtrips(ccsMinimizeRoundtrips)
                 .fetchFields(randomFetchFields)
                 .runtimeMappings(randomRuntimeMappings());
         } catch (IOException ex) {
@@ -101,7 +104,7 @@ public class EqlSearchRequestTests extends AbstractBWCSerializationTestCase<EqlS
 
     @Override
     protected EqlSearchRequest doParseInstance(XContentParser parser) {
-        return EqlSearchRequest.fromXContent(parser).indices(defaultTestIndex);
+        return EqlSearchRequest.fromXContent(parser).indices(defaultTestIndex).ccsMinimizeRoundtrips(ccsMinimizeRoundtrips);
     }
 
     @Override
@@ -116,6 +119,7 @@ public class EqlSearchRequestTests extends AbstractBWCSerializationTestCase<EqlS
         mutatedInstance.size(instance.size());
         mutatedInstance.fetchSize(instance.fetchSize());
         mutatedInstance.query(instance.query());
+        mutatedInstance.ccsMinimizeRoundtrips(version.onOrAfter(Version.V_7_15_0) == false || instance.ccsMinimizeRoundtrips());
         mutatedInstance.waitForCompletionTimeout(instance.waitForCompletionTimeout());
         mutatedInstance.keepAlive(instance.keepAlive());
         mutatedInstance.keepOnCompletion(instance.keepOnCompletion());

+ 3 - 2
x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/analysis/CancellationTests.java

@@ -18,6 +18,7 @@ import org.elasticsearch.client.Client;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.node.DiscoveryNode;
 import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.breaker.NoopCircuitBreaker;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.tasks.TaskCancelHelper;
@@ -83,8 +84,8 @@ public class CancellationTests extends ESTestCase {
         IndexResolver indexResolver = indexResolver(client);
         PlanExecutor planExecutor = planExecutor(client, indexResolver);
         CountDownLatch countDownLatch = new CountDownLatch(1);
-        TransportEqlSearchAction.operation(planExecutor, task, new EqlSearchRequest().query("foo where blah"), "",
-            transportService, mockClusterService, new ActionListener<>() {
+        TransportEqlSearchAction.operation(planExecutor, task, new EqlSearchRequest().indices(Strings.EMPTY_ARRAY).query("foo where blah"),
+            "", transportService, mockClusterService, new ActionListener<>() {
                 @Override
                 public void onResponse(EqlSearchResponse eqlSearchResponse) {
                     fail("Shouldn't be here");