2121import org .elasticsearch .action .ActionType ;
2222import org .elasticsearch .action .OriginalIndices ;
2323import org .elasticsearch .action .RemoteClusterActionType ;
24+ import org .elasticsearch .action .ResolvedIndexExpressions ;
2425import org .elasticsearch .action .support .AbstractThreadedActionListener ;
2526import org .elasticsearch .action .support .ActionFilters ;
2627import org .elasticsearch .action .support .ChannelActionListener ;
3839import org .elasticsearch .common .regex .Regex ;
3940import org .elasticsearch .common .util .Maps ;
4041import org .elasticsearch .common .util .concurrent .AbstractRunnable ;
42+ import org .elasticsearch .common .util .concurrent .ConcurrentCollections ;
43+ import org .elasticsearch .common .util .concurrent .CountDown ;
4144import org .elasticsearch .common .util .concurrent .EsExecutors ;
4245import org .elasticsearch .common .util .concurrent .ThrottledTaskRunner ;
4346import org .elasticsearch .core .Nullable ;
4952import org .elasticsearch .injection .guice .Inject ;
5053import org .elasticsearch .logging .LogManager ;
5154import org .elasticsearch .logging .Logger ;
55+ import org .elasticsearch .node .internal .TerminationHandler ;
5256import org .elasticsearch .search .SearchService ;
57+ import org .elasticsearch .search .crossproject .CrossProjectIndexResolutionValidator ;
5358import org .elasticsearch .search .crossproject .CrossProjectModeDecider ;
5459import org .elasticsearch .tasks .CancellableTask ;
5560import org .elasticsearch .tasks .Task ;
6469import java .io .IOException ;
6570import java .util .ArrayList ;
6671import java .util .Arrays ;
72+ import java .util .Collection ;
6773import java .util .Collections ;
6874import java .util .Comparator ;
6975import java .util .HashMap ;
7278import java .util .List ;
7379import java .util .Map ;
7480import java .util .Set ;
81+ import java .util .concurrent .ConcurrentHashMap ;
7582import java .util .concurrent .Executor ;
7683import java .util .concurrent .atomic .AtomicBoolean ;
7784import java .util .concurrent .atomic .AtomicReference ;
@@ -182,6 +189,7 @@ private <R extends ActionResponse> void doExecuteForked(
182189 LinkedRequestExecutor <R > linkedRequestExecutor ,
183190 ActionListener <R > listener
184191 ) {
192+ final boolean crossProjectEnabled = crossProjectModeDecider .resolvesCrossProject (request );
185193 if (ccsCheckCompatibility ) {
186194 checkCCSVersionCompatibility (request );
187195 }
@@ -309,6 +317,44 @@ private <R extends ActionResponse> void doExecuteForked(
309317 );
310318 requestDispatcher .execute ();
311319
320+ /*
321+ * We need to run the Cross Project Search reconciliation but only after we're heard back from all the linked projects.
322+ * It is also possible that some linked projects may respond back with an error instead of a valid response. To facilitate
323+ * this, we track each response, irrespective of whether it's valid or not, and then perform the reconciliation.
324+ */
325+ CountDown countDownResponses = new CountDown (remoteClusterIndices .size ());
326+ Map <String , ResolvedIndexExpressions > linkedProjectsResponses = ConcurrentCollections .newConcurrentMap ();
327+ Runnable crossProjectReconciler = () -> {
328+ if (countDownResponses .countDown () && crossProjectEnabled ) {
329+ /*
330+ * This happens when one or more linked projects respond with an error instead of a valid response -- say, networking
331+ * error.
332+ */
333+ if (linkedProjectsResponses .size () != remoteClusterIndices .size ()) {
334+ listener .onFailure (
335+ new IllegalArgumentException (
336+ "Invalid number of responses received: "
337+ + linkedProjectsResponses .size ()
338+ + " vs expected "
339+ + remoteClusterIndices .size ()
340+ )
341+ );
342+ return ;
343+ }
344+
345+ Exception validationEx = CrossProjectIndexResolutionValidator .validate (
346+ request .indicesOptions (),
347+ null ,
348+ request .getResolvedIndexExpressions (),
349+ linkedProjectsResponses
350+ );
351+
352+ if (validationEx != null ) {
353+ listener .onFailure (validationEx );
354+ }
355+ }
356+ };
357+
312358 // this is the cross cluster part of this API - we force the other cluster to not merge the results but instead
313359 // send us back all individual index results.
314360 for (Map .Entry <String , OriginalIndices > remoteIndices : remoteClusterIndices .entrySet ()) {
@@ -322,6 +368,10 @@ private <R extends ActionResponse> void doExecuteForked(
322368 crossProjectModeDecider
323369 );
324370 ActionListener <FieldCapabilitiesResponse > remoteListener = ActionListener .wrap (response -> {
371+ assert response .getResolvedIndexExpressions () != null
372+ : "Resolved index expressions from [" + clusterAlias + "] are null" ;
373+ linkedProjectsResponses .put (clusterAlias , response .getResolvedIndexExpressions ());
374+
325375 for (FieldCapabilitiesIndexResponse resp : response .getIndexResponses ()) {
326376 String indexName = RemoteClusterAware .buildRemoteIndexName (clusterAlias , resp .getIndexName ());
327377 handleIndexResponse .accept (
@@ -346,10 +396,12 @@ private <R extends ActionResponse> void doExecuteForked(
346396 }
347397 return TransportVersion .min (lhs , rhs );
348398 });
399+ crossProjectReconciler .run ();
349400 }, ex -> {
350401 for (String index : originalIndices .indices ()) {
351402 handleIndexFailure .accept (RemoteClusterAware .buildRemoteIndexName (clusterAlias , index ), ex );
352403 }
404+ crossProjectReconciler .run ();
353405 });
354406
355407 SubscribableListener <Transport .Connection > connectionListener = new SubscribableListener <>();
0 commit comments