2323import org .elasticsearch .xpack .esql .plan .logical .LogicalPlan ;
2424
2525import java .util .Collections ;
26+ import java .util .HashMap ;
2627import java .util .HashSet ;
2728import java .util .List ;
2829import java .util .Map ;
2930import java .util .Set ;
3031
31- class CcsUtils {
32+ class EsqlSessionCCSUtils {
3233
33- private CcsUtils () {}
34+ private EsqlSessionCCSUtils () {}
35+
36+ // visible for testing
37+ static Map <String , FieldCapabilitiesFailure > determineUnavailableRemoteClusters (List <FieldCapabilitiesFailure > failures ) {
38+ Map <String , FieldCapabilitiesFailure > unavailableRemotes = new HashMap <>();
39+ for (FieldCapabilitiesFailure failure : failures ) {
40+ if (ExceptionsHelper .isRemoteUnavailableException (failure .getException ())) {
41+ for (String indexExpression : failure .getIndices ()) {
42+ if (indexExpression .indexOf (RemoteClusterAware .REMOTE_CLUSTER_INDEX_SEPARATOR ) > 0 ) {
43+ unavailableRemotes .put (RemoteClusterAware .parseClusterAlias (indexExpression ), failure );
44+ }
45+ }
46+ }
47+ }
48+ return unavailableRemotes ;
49+ }
3450
3551 /**
3652 * ActionListener that receives LogicalPlan or error from logical planning.
@@ -46,70 +62,73 @@ abstract static class CssPartialErrorsActionListener implements ActionListener<L
4662 this .listener = listener ;
4763 }
4864
49- /**
50- * Whether to return an empty result (HTTP status 200) for a CCS rather than a top level 4xx/5xx error.
51- *
52- * For cases where field-caps had no indices to search and the remotes were unavailable, we
53- * return an empty successful response (200) if all remotes are marked with skip_unavailable=true.
54- *
55- * Note: a follow-on PR will expand this logic to handle cases where no indices could be found to match
56- * on any of the requested clusters.
57- */
58- private boolean returnSuccessWithEmptyResult (Exception e ) {
59- if (executionInfo .isCrossClusterSearch () == false ) {
60- return false ;
65+ @ Override
66+ public void onFailure (Exception e ) {
67+ if (returnSuccessWithEmptyResult (executionInfo , e )) {
68+ updateExecutionInfoToReturnEmptyResult (executionInfo , e );
69+ listener .onResponse (new Result (Analyzer .NO_FIELDS , Collections .emptyList (), Collections .emptyList (), executionInfo ));
70+ } else {
71+ listener .onFailure (e );
6172 }
73+ }
74+ }
6275
63- if (e instanceof NoClustersToSearchException || ExceptionsHelper .isRemoteUnavailableException (e )) {
64- for (String clusterAlias : executionInfo .clusterAliases ()) {
65- if (executionInfo .isSkipUnavailable (clusterAlias ) == false
66- && clusterAlias .equals (RemoteClusterAware .LOCAL_CLUSTER_GROUP_KEY ) == false ) {
67- return false ;
68- }
76+ /**
77+ * Whether to return an empty result (HTTP status 200) for a CCS rather than a top level 4xx/5xx error.
78+ *
79+ * For cases where field-caps had no indices to search and the remotes were unavailable, we
80+ * return an empty successful response (200) if all remotes are marked with skip_unavailable=true.
81+ *
82+ * Note: a follow-on PR will expand this logic to handle cases where no indices could be found to match
83+ * on any of the requested clusters.
84+ */
85+ static boolean returnSuccessWithEmptyResult (EsqlExecutionInfo executionInfo , Exception e ) {
86+ if (executionInfo .isCrossClusterSearch () == false ) {
87+ return false ;
88+ }
89+
90+ if (e instanceof NoClustersToSearchException || ExceptionsHelper .isRemoteUnavailableException (e )) {
91+ for (String clusterAlias : executionInfo .clusterAliases ()) {
92+ if (executionInfo .isSkipUnavailable (clusterAlias ) == false
93+ && clusterAlias .equals (RemoteClusterAware .LOCAL_CLUSTER_GROUP_KEY ) == false ) {
94+ return false ;
6995 }
70- return true ;
7196 }
72- return false ;
97+ return true ;
7398 }
99+ return false ;
100+ }
74101
75- @ Override
76- public void onFailure (Exception e ) {
77- if (returnSuccessWithEmptyResult (e )) {
78- executionInfo .markEndQuery ();
79- Exception exceptionForResponse ;
80- if (e instanceof ConnectTransportException ) {
81- // when field-caps has no field info (since no clusters could be connected to or had matching indices)
82- // it just throws the first exception in its list, so this odd special handling is here is to avoid
83- // having one specific remote alias name in all failure lists in the metadata response
84- exceptionForResponse = new RemoteTransportException (
85- "connect_transport_exception - unable to connect to remote cluster" ,
86- null
87- );
102+ static void updateExecutionInfoToReturnEmptyResult (EsqlExecutionInfo executionInfo , Exception e ) {
103+ executionInfo .markEndQuery ();
104+ Exception exceptionForResponse ;
105+ if (e instanceof ConnectTransportException ) {
106+ // when field-caps has no field info (since no clusters could be connected to or had matching indices)
107+ // it just throws the first exception in its list, so this odd special handling is here is to avoid
108+ // having one specific remote alias name in all failure lists in the metadata response
109+ exceptionForResponse = new RemoteTransportException ("connect_transport_exception - unable to connect to remote cluster" , null );
110+ } else {
111+ exceptionForResponse = e ;
112+ }
113+ for (String clusterAlias : executionInfo .clusterAliases ()) {
114+ executionInfo .swapCluster (clusterAlias , (k , v ) -> {
115+ EsqlExecutionInfo .Cluster .Builder builder = new EsqlExecutionInfo .Cluster .Builder (v ).setTook (executionInfo .overallTook ())
116+ .setTotalShards (0 )
117+ .setSuccessfulShards (0 )
118+ .setSkippedShards (0 )
119+ .setFailedShards (0 );
120+ if (RemoteClusterAware .LOCAL_CLUSTER_GROUP_KEY .equals (clusterAlias )) {
121+ // never mark local cluster as skipped
122+ builder .setStatus (EsqlExecutionInfo .Cluster .Status .SUCCESSFUL );
88123 } else {
89- exceptionForResponse = e ;
90- }
91- for (String clusterAlias : executionInfo .clusterAliases ()) {
92- executionInfo .swapCluster (clusterAlias , (k , v ) -> {
93- EsqlExecutionInfo .Cluster .Builder builder = new EsqlExecutionInfo .Cluster .Builder (v ).setTook (
94- executionInfo .overallTook ()
95- ).setTotalShards (0 ).setSuccessfulShards (0 ).setSkippedShards (0 ).setFailedShards (0 );
96- if (RemoteClusterAware .LOCAL_CLUSTER_GROUP_KEY .equals (clusterAlias )) {
97- // never mark local cluster as skipped
98- builder .setStatus (EsqlExecutionInfo .Cluster .Status .SUCCESSFUL );
99- } else {
100- builder .setStatus (EsqlExecutionInfo .Cluster .Status .SKIPPED );
101- // add this exception to the failures list only if there is no failure already recorded there
102- if (v .getFailures () == null || v .getFailures ().size () == 0 ) {
103- builder .setFailures (List .of (new ShardSearchFailure (exceptionForResponse )));
104- }
105- }
106- return builder .build ();
107- });
124+ builder .setStatus (EsqlExecutionInfo .Cluster .Status .SKIPPED );
125+ // add this exception to the failures list only if there is no failure already recorded there
126+ if (v .getFailures () == null || v .getFailures ().size () == 0 ) {
127+ builder .setFailures (List .of (new ShardSearchFailure (exceptionForResponse )));
128+ }
108129 }
109- listener .onResponse (new Result (Analyzer .NO_FIELDS , Collections .emptyList (), Collections .emptyList (), executionInfo ));
110- } else {
111- listener .onFailure (e );
112- }
130+ return builder .build ();
131+ });
113132 }
114133 }
115134
0 commit comments