From 3dd69d6ae46d687b52ebc8cb3478c0d288574ebc Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 31 Oct 2025 20:11:42 +0000 Subject: [PATCH 1/6] Enable flat index expressions support for `_field_caps` --- .../fieldcaps/FieldCapabilitiesRequest.java | 5 +++++ .../TransportFieldCapabilitiesAction.java | 19 +++++++++++++++++-- .../action/RestFieldCapabilitiesAction.java | 12 +++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index c4c974ad6b668..4869a27be0b66 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -243,6 +243,11 @@ public boolean allowsRemoteIndices() { return true; } + @Override + public boolean allowsCrossProject() { + return true; + } + @Override public boolean includeDataStreams() { return true; diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index ffe14bcc6af50..fd8a6cc921cf3 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -50,6 +50,7 @@ import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; import org.elasticsearch.search.SearchService; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; @@ -81,6 +82,7 @@ import java.util.stream.Collectors; import static org.elasticsearch.action.search.TransportSearchHelper.checkCCSVersionCompatibility; +import static org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator.indicesOptionsForCrossProjectFanout; public class TransportFieldCapabilitiesAction extends HandledTransportAction { public static final String NAME = "indices:data/read/field_caps"; @@ -102,6 +104,7 @@ public class TransportFieldCapabilitiesAction extends HandledTransportAction void doExecuteForked( for (Map.Entry remoteIndices : remoteClusterIndices.entrySet()) { String clusterAlias = remoteIndices.getKey(); OriginalIndices originalIndices = remoteIndices.getValue(); - FieldCapabilitiesRequest remoteRequest = prepareRemoteRequest(clusterAlias, request, originalIndices, nowInMillis); + FieldCapabilitiesRequest remoteRequest = prepareRemoteRequest( + clusterAlias, + request, + originalIndices, + nowInMillis, + crossProjectModeDecider + ); ActionListener remoteListener = ActionListener.wrap(response -> { for (FieldCapabilitiesIndexResponse resp : response.getIndexResponses()) { String indexName = RemoteClusterAware.buildRemoteIndexName(clusterAlias, resp.getIndexName()); @@ -475,12 +485,17 @@ private static FieldCapabilitiesRequest prepareRemoteRequest( String clusterAlias, FieldCapabilitiesRequest request, OriginalIndices originalIndices, - long nowInMillis + long nowInMillis, + CrossProjectModeDecider crossProjectModeDecider ) { FieldCapabilitiesRequest remoteRequest = new FieldCapabilitiesRequest(); remoteRequest.clusterAlias(clusterAlias); remoteRequest.setMergeResults(false); // we need to merge on this node remoteRequest.indicesOptions(originalIndices.indicesOptions()); + if (crossProjectModeDecider.resolvesCrossProject(request)) { + remoteRequest.indicesOptions(indicesOptionsForCrossProjectFanout(remoteRequest.indicesOptions())); + } + remoteRequest.indices(originalIndices.indices()); remoteRequest.fields(request.fields()); remoteRequest.filters(request.filters()); diff --git a/server/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java index 11b5c97aeb48c..faae60a638216 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/RestFieldCapabilitiesAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -33,9 +34,11 @@ public class RestFieldCapabilitiesAction extends BaseRestHandler { private final Settings settings; + private final CrossProjectModeDecider crossProjectModeDecider; public RestFieldCapabilitiesAction(Settings settings) { this.settings = settings; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -55,7 +58,8 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException { - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { + final boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); + if (crossProjectModeDecider.crossProjectEnabled()) { // accept but drop project_routing param until fully supported request.param("project_routing"); } @@ -63,6 +67,12 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC final String[] indices = Strings.splitStringByCommaToArray(request.param("index")); final FieldCapabilitiesRequest fieldRequest = new FieldCapabilitiesRequest(); fieldRequest.indices(indices); + if (crossProjectEnabled && fieldRequest.allowsCrossProject()) { + var cpsIdxOpts = IndicesOptions.builder(fieldRequest.indicesOptions()) + .crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true)) + .build(); + fieldRequest.indicesOptions(cpsIdxOpts); + } fieldRequest.indicesOptions(IndicesOptions.fromRequest(request, fieldRequest.indicesOptions())); fieldRequest.includeUnmapped(request.paramAsBoolean("include_unmapped", false)); From dda89a573c322c6c952c0cac3421d559105fa5f9 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 3 Nov 2025 16:36:51 +0000 Subject: [PATCH 2/6] Track resolved index expressions and perform reconciliation --- .../fieldcaps/FieldCapabilitiesRequest.java | 12 +++++ .../fieldcaps/FieldCapabilitiesResponse.java | 29 +++++++++-- .../TransportFieldCapabilitiesAction.java | 52 +++++++++++++++++++ 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index 4869a27be0b66..ad1d834fe77f9 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -14,6 +14,7 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.LegacyActionRequest; +import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.ValidateActions; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.common.Strings; @@ -70,6 +71,7 @@ public final class FieldCapabilitiesRequest extends LegacyActionRequest implemen private QueryBuilder indexFilter; private Map runtimeFields = Collections.emptyMap(); private Long nowInMillis; + private ResolvedIndexExpressions resolvedIndexExpressions; public FieldCapabilitiesRequest(StreamInput in) throws IOException { super(in); @@ -248,6 +250,16 @@ public boolean allowsCrossProject() { return true; } + @Override + public void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) { + this.resolvedIndexExpressions = expressions; + } + + @Override + public ResolvedIndexExpressions getResolvedIndexExpressions() { + return resolvedIndexExpressions; + } + @Override public boolean includeDataStreams() { return true; diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java index daea9b1a5fc01..e6d06b77689dc 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -11,6 +11,7 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; @@ -45,21 +46,22 @@ public class FieldCapabilitiesResponse extends ActionResponse implements Chunked private final List failures; private final List indexResponses; private final TransportVersion minTransportVersion; + private final ResolvedIndexExpressions resolvedIndexExpressions; public FieldCapabilitiesResponse( String[] indices, Map> fields, List failures ) { - this(indices, fields, Collections.emptyList(), failures, null); + this(indices, fields, Collections.emptyList(), failures, null, null); } public FieldCapabilitiesResponse(String[] indices, Map> fields) { - this(indices, fields, Collections.emptyList(), Collections.emptyList(), null); + this(indices, fields, Collections.emptyList(), Collections.emptyList(), null, null); } public FieldCapabilitiesResponse(List indexResponses, List failures) { - this(Strings.EMPTY_ARRAY, Collections.emptyMap(), indexResponses, failures, null); + this(Strings.EMPTY_ARRAY, Collections.emptyMap(), indexResponses, failures, null, null); } private FieldCapabilitiesResponse( @@ -67,13 +69,25 @@ private FieldCapabilitiesResponse( Map> fields, List indexResponses, List failures, - TransportVersion minTransportVersion + TransportVersion minTransportVersion, + @Nullable ResolvedIndexExpressions resolvedIndexExpressions ) { this.fields = Objects.requireNonNull(fields); this.indexResponses = Objects.requireNonNull(indexResponses); this.indices = indices; this.failures = failures; this.minTransportVersion = minTransportVersion; + this.resolvedIndexExpressions = resolvedIndexExpressions; + } + + private FieldCapabilitiesResponse( + String[] indices, + Map> fields, + List indexResponses, + List failures, + TransportVersion minTransportVersion + ) { + this(indices, fields, indexResponses, failures, minTransportVersion, null); } public FieldCapabilitiesResponse(StreamInput in) throws IOException { @@ -84,6 +98,7 @@ public FieldCapabilitiesResponse(StreamInput in) throws IOException { this.minTransportVersion = in.getTransportVersion().supports(MIN_TRANSPORT_VERSION) ? in.readOptional(TransportVersion::readVersion) : null; + this.resolvedIndexExpressions = in.readOptionalWriteable(ResolvedIndexExpressions::new); } /** @@ -172,6 +187,7 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getTransportVersion().supports(MIN_TRANSPORT_VERSION)) { out.writeOptional((Writer) (o, v) -> TransportVersion.writeVersion(v, o), minTransportVersion); } + out.writeOptionalWriteable(this.resolvedIndexExpressions); } private static void writeField(StreamOutput out, Map map) throws IOException { @@ -268,4 +284,9 @@ public FieldCapabilitiesResponse build() { return new FieldCapabilitiesResponse(indices, fields, indexResponses, failures, minTransportVersion); } } + + @Nullable + public ResolvedIndexExpressions getResolvedIndexExpressions() { + return resolvedIndexExpressions; + } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index fd8a6cc921cf3..1a3bea672ff38 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -21,6 +21,7 @@ import org.elasticsearch.action.ActionType; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.RemoteClusterActionType; +import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.support.AbstractThreadedActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.ChannelActionListener; @@ -38,6 +39,8 @@ import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ThrottledTaskRunner; import org.elasticsearch.core.Nullable; @@ -49,7 +52,9 @@ import org.elasticsearch.injection.guice.Inject; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; +import org.elasticsearch.node.internal.TerminationHandler; import org.elasticsearch.search.SearchService; +import org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator; import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; @@ -64,6 +69,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -72,6 +78,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -182,6 +189,7 @@ private void doExecuteForked( LinkedRequestExecutor linkedRequestExecutor, ActionListener listener ) { + final boolean crossProjectEnabled = crossProjectModeDecider.resolvesCrossProject(request); if (ccsCheckCompatibility) { checkCCSVersionCompatibility(request); } @@ -309,6 +317,44 @@ private void doExecuteForked( ); requestDispatcher.execute(); + /* + * We need to run the Cross Project Search reconciliation but only after we're heard back from all the linked projects. + * It is also possible that some linked projects may respond back with an error instead of a valid response. To facilitate + * this, we track each response, irrespective of whether it's valid or not, and then perform the reconciliation. + */ + CountDown countDownResponses = new CountDown(remoteClusterIndices.size()); + Map linkedProjectsResponses = ConcurrentCollections.newConcurrentMap(); + Runnable crossProjectReconciler = () -> { + if (countDownResponses.countDown() && crossProjectEnabled) { + /* + * This happens when one or more linked projects respond with an error instead of a valid response -- say, networking + * error. + */ + if (linkedProjectsResponses.size() != remoteClusterIndices.size()) { + listener.onFailure( + new IllegalArgumentException( + "Invalid number of responses received: " + + linkedProjectsResponses.size() + + " vs expected " + + remoteClusterIndices.size() + ) + ); + return; + } + + Exception validationEx = CrossProjectIndexResolutionValidator.validate( + request.indicesOptions(), + null, + request.getResolvedIndexExpressions(), + linkedProjectsResponses + ); + + if (validationEx != null) { + listener.onFailure(validationEx); + } + } + }; + // this is the cross cluster part of this API - we force the other cluster to not merge the results but instead // send us back all individual index results. for (Map.Entry remoteIndices : remoteClusterIndices.entrySet()) { @@ -322,6 +368,10 @@ private void doExecuteForked( crossProjectModeDecider ); ActionListener remoteListener = ActionListener.wrap(response -> { + assert response.getResolvedIndexExpressions() != null + : "Resolved index expressions from [" + clusterAlias + "] are null"; + linkedProjectsResponses.put(clusterAlias, response.getResolvedIndexExpressions()); + for (FieldCapabilitiesIndexResponse resp : response.getIndexResponses()) { String indexName = RemoteClusterAware.buildRemoteIndexName(clusterAlias, resp.getIndexName()); handleIndexResponse.accept( @@ -346,10 +396,12 @@ private void doExecuteForked( } return TransportVersion.min(lhs, rhs); }); + crossProjectReconciler.run(); }, ex -> { for (String index : originalIndices.indices()) { handleIndexFailure.accept(RemoteClusterAware.buildRemoteIndexName(clusterAlias, index), ex); } + crossProjectReconciler.run(); }); SubscribableListener connectionListener = new SubscribableListener<>(); From 1042c5715ef9f3b8da8c2d7cc9d6d51317aaeec3 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 3 Nov 2025 16:45:01 +0000 Subject: [PATCH 3/6] [CI] Auto commit changes from spotless --- .../action/fieldcaps/TransportFieldCapabilitiesAction.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index 1a3bea672ff38..46eef3187e672 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -52,7 +52,6 @@ import org.elasticsearch.injection.guice.Inject; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; -import org.elasticsearch.node.internal.TerminationHandler; import org.elasticsearch.search.SearchService; import org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator; import org.elasticsearch.search.crossproject.CrossProjectModeDecider; @@ -69,7 +68,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -78,7 +76,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; From 31ddb67cbc6c4ad3e4c81137f67988200f0100be Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 3 Nov 2025 17:05:29 +0000 Subject: [PATCH 4/6] Clarify comment --- .../action/fieldcaps/TransportFieldCapabilitiesAction.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index 723e571967803..020ab4dd2ee4c 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -376,7 +376,8 @@ private void doExecuteForked( /* * We need to run the Cross Project Search reconciliation but only after we're heard back from all the linked projects. * It is also possible that some linked projects may respond back with an error instead of a valid response. To facilitate - * this, we track each response, irrespective of whether it's valid or not, and then perform the reconciliation. + * this, we use `CountDown` and track each response, irrespective of whether it's valid or not, and then perform the + * reconciliation when it has counted down and the request is a resolvable CPS request. */ CountDown countDownResponses = new CountDown(remoteClusterIndices.size()); Map linkedProjectsResponses = ConcurrentCollections.newConcurrentMap(); From 31ea3680566c7d83b71f5076b6f4d5cec3cabe35 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 3 Nov 2025 17:46:31 +0000 Subject: [PATCH 5/6] Fix misc. issues --- .../fieldcaps/FieldCapabilitiesResponse.java | 8 ++- .../TransportFieldCapabilitiesAction.java | 62 ++++++++++--------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java index 8c3c6449e85ac..869cae875a618 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -320,6 +320,7 @@ public static class Builder { private List indexResponses = Collections.emptyList(); private List failures = Collections.emptyList(); private TransportVersion minTransportVersion = null; + private ResolvedIndexExpressions resolvedIndexExpressions = null; private Builder() {} @@ -366,6 +367,11 @@ public Builder withMinTransportVersion(TransportVersion minTransportVersion) { return this; } + public Builder withResolvedIndexExpressions(ResolvedIndexExpressions resolvedIndexExpressions) { + this.resolvedIndexExpressions = resolvedIndexExpressions; + return this; + } + public FieldCapabilitiesResponse build() { return new FieldCapabilitiesResponse( indices, @@ -375,7 +381,7 @@ public FieldCapabilitiesResponse build() { indexResponses, failures, minTransportVersion, - null + resolvedIndexExpressions ); } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index 020ab4dd2ee4c..9e123b6bfee1e 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -379,38 +379,43 @@ private void doExecuteForked( * this, we use `CountDown` and track each response, irrespective of whether it's valid or not, and then perform the * reconciliation when it has counted down and the request is a resolvable CPS request. */ - CountDown countDownResponses = new CountDown(remoteClusterIndices.size()); + Runnable crossProjectReconciler; Map linkedProjectsResponses = ConcurrentCollections.newConcurrentMap(); - Runnable crossProjectReconciler = () -> { - if (countDownResponses.countDown() && crossProjectEnabled) { - /* - * This happens when one or more linked projects respond with an error instead of a valid response -- say, networking - * error. - */ - if (linkedProjectsResponses.size() != remoteClusterIndices.size()) { - listener.onFailure( - new IllegalArgumentException( - "Invalid number of responses received: " - + linkedProjectsResponses.size() - + " vs expected " - + remoteClusterIndices.size() - ) - ); - return; - } + if (remoteClusterIndices.size() > 0) { + CountDown countDownResponses = new CountDown(remoteClusterIndices.size()); + crossProjectReconciler = () -> { + if (countDownResponses.countDown() && crossProjectEnabled) { + /* + * This happens when one or more linked projects respond with an error instead of a valid response -- say + * networking error. + */ + if (linkedProjectsResponses.size() != remoteClusterIndices.size()) { + listener.onFailure( + new IllegalArgumentException( + "Invalid number of responses received: " + + linkedProjectsResponses.size() + + " vs expected " + + remoteClusterIndices.size() + ) + ); + return; + } - Exception validationEx = CrossProjectIndexResolutionValidator.validate( - request.indicesOptions(), - null, - request.getResolvedIndexExpressions(), - linkedProjectsResponses - ); + Exception validationEx = CrossProjectIndexResolutionValidator.validate( + request.indicesOptions(), + null, + request.getResolvedIndexExpressions(), + linkedProjectsResponses + ); - if (validationEx != null) { - listener.onFailure(validationEx); + if (validationEx != null) { + listener.onFailure(validationEx); + } } - } - }; + }; + } else { + crossProjectReconciler = () -> {}; + } // this is the cross cluster part of this API - we force the other cluster to not merge the results but instead // send us back all individual index results. @@ -631,6 +636,7 @@ private static void mergeIndexResponses( .withResolvedRemotelyBuilder(resolvedRemotely) .withMinTransportVersion(minTransportVersion.get()) .withFailures(failures) + .withResolvedIndexExpressions(request.getResolvedIndexExpressions()) .build() ); } From 72916cfb222f10c36c5a450770f977d01d49c238 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 3 Nov 2025 18:40:40 +0000 Subject: [PATCH 6/6] Modify guard clause --- .../action/fieldcaps/TransportFieldCapabilitiesAction.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java index 9e123b6bfee1e..cb4f3ad8ef30e 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java @@ -374,17 +374,18 @@ private void doExecuteForked( requestDispatcher.execute(); /* - * We need to run the Cross Project Search reconciliation but only after we're heard back from all the linked projects. + * We need to run the Cross Project Search reconciliation but only after we've heard back from all the linked projects. * It is also possible that some linked projects may respond back with an error instead of a valid response. To facilitate * this, we use `CountDown` and track each response, irrespective of whether it's valid or not, and then perform the * reconciliation when it has counted down and the request is a resolvable CPS request. */ Runnable crossProjectReconciler; Map linkedProjectsResponses = ConcurrentCollections.newConcurrentMap(); - if (remoteClusterIndices.size() > 0) { + // Run the reconciler only if there are linked projects and CPS is enabled. + if (remoteClusterIndices.isEmpty() == false && crossProjectEnabled) { CountDown countDownResponses = new CountDown(remoteClusterIndices.size()); crossProjectReconciler = () -> { - if (countDownResponses.countDown() && crossProjectEnabled) { + if (countDownResponses.countDown()) { /* * This happens when one or more linked projects respond with an error instead of a valid response -- say * networking error.