|
26 | 26 | import org.elasticsearch.indices.IndicesExpressionGrouper; |
27 | 27 | import org.elasticsearch.logging.LogManager; |
28 | 28 | import org.elasticsearch.logging.Logger; |
| 29 | +import org.elasticsearch.transport.RemoteClusterAware; |
29 | 30 | import org.elasticsearch.xpack.esql.VerificationException; |
30 | 31 | import org.elasticsearch.xpack.esql.action.EsqlExecutionInfo; |
31 | 32 | import org.elasticsearch.xpack.esql.action.EsqlQueryRequest; |
|
103 | 104 | import java.util.Set; |
104 | 105 | import java.util.function.Function; |
105 | 106 | import java.util.stream.Collectors; |
| 107 | +import java.util.stream.Stream; |
106 | 108 |
|
107 | 109 | import static org.elasticsearch.index.query.QueryBuilders.boolQuery; |
108 | 110 | import static org.elasticsearch.xpack.esql.core.util.StringUtils.WILDCARD; |
@@ -350,7 +352,7 @@ public void analyzedPlan( |
350 | 352 | .<PreAnalysisResult>andThen((l, preAnalysisResult) -> resolveInferences(preAnalysis.inferencePlans, preAnalysisResult, l)); |
351 | 353 | // first resolve the lookup indices, then the main indices |
352 | 354 | for (var index : preAnalysis.lookupIndices) { |
353 | | - listener = listener.andThen((l, preAnalysisResult) -> { preAnalyzeLookupIndex(index, preAnalysisResult, l); }); |
| 355 | + listener = listener.andThen((l, preAnalysisResult) -> { preAnalyzeLookupIndex(index, preAnalysisResult, executionInfo, l); }); |
354 | 356 | } |
355 | 357 | listener.<PreAnalysisResult>andThen((l, result) -> { |
356 | 358 | // resolve the main indices |
@@ -389,16 +391,110 @@ public void analyzedPlan( |
389 | 391 | }).addListener(logicalPlanListener); |
390 | 392 | } |
391 | 393 |
|
392 | | - private void preAnalyzeLookupIndex(IndexPattern table, PreAnalysisResult result, ActionListener<PreAnalysisResult> listener) { |
393 | | - Set<String> fieldNames = result.wildcardJoinIndices().contains(table.indexPattern()) ? IndexResolver.ALL_FIELDS : result.fieldNames; |
| 394 | + private void preAnalyzeLookupIndex( |
| 395 | + IndexPattern table, |
| 396 | + PreAnalysisResult result, |
| 397 | + EsqlExecutionInfo executionInfo, |
| 398 | + ActionListener<PreAnalysisResult> listener |
| 399 | + ) { |
| 400 | + String localPattern = table.indexPattern(); |
| 401 | + assert RemoteClusterAware.isRemoteIndexName(localPattern) == false |
| 402 | + : "Lookup index name should not include remote, but got: " + localPattern; |
| 403 | + Set<String> fieldNames = result.wildcardJoinIndices().contains(localPattern) ? IndexResolver.ALL_FIELDS : result.fieldNames; |
| 404 | + // Get the list of active clusters for the lookup index |
| 405 | + Stream<EsqlExecutionInfo.Cluster> clusters = executionInfo.getClusterStates(EsqlExecutionInfo.Cluster.Status.RUNNING); |
| 406 | + StringBuilder patternWithRemotes = new StringBuilder(localPattern); |
| 407 | + // Create a pattern with all active remote clusters |
| 408 | + clusters.forEach(cluster -> { |
| 409 | + String clusterAlias = cluster.getClusterAlias(); |
| 410 | + if (RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY.equals(clusterAlias)) { |
| 411 | + // Skip the local cluster, as it is already included in the localPattern |
| 412 | + return; |
| 413 | + } |
| 414 | + patternWithRemotes.append(",").append(clusterAlias).append(":").append(localPattern); |
| 415 | + }); |
394 | 416 | // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types |
395 | 417 | indexResolver.resolveAsMergedMapping( |
396 | | - table.indexPattern(), |
| 418 | + patternWithRemotes.toString(), |
397 | 419 | fieldNames, |
398 | 420 | null, |
399 | | - listener.map(indexResolution -> result.addLookupIndexResolution(table.indexPattern(), indexResolution)) |
| 421 | + listener.map(indexResolution -> receiveLookupIndexResolution(result, localPattern, executionInfo, indexResolution)) |
400 | 422 | ); |
401 | | - // TODO: Verify that the resolved index actually has indexMode: "lookup" |
| 423 | + } |
| 424 | + |
| 425 | + private PreAnalysisResult receiveLookupIndexResolution( |
| 426 | + PreAnalysisResult result, |
| 427 | + String index, |
| 428 | + EsqlExecutionInfo executionInfo, |
| 429 | + IndexResolution newIndexResolution |
| 430 | + ) { |
| 431 | + EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, newIndexResolution.unavailableClusters()); |
| 432 | + if (newIndexResolution.isValid() == false) { |
| 433 | + // If the index resolution is invalid, don't bother with the rest of the analysis |
| 434 | + return result.addLookupIndexResolution(index, newIndexResolution); |
| 435 | + } |
| 436 | + // Collect resolved clusters from the index resolution, verify that each cluster has a single resolution for the lookup index |
| 437 | + Map<String, String> clustersWithResolvedIndices = new HashMap<>(newIndexResolution.resolvedIndices().size()); |
| 438 | + newIndexResolution.get().indexNameWithModes().forEach((indexName, indexMode) -> { |
| 439 | + if (indexMode != IndexMode.LOOKUP) { |
| 440 | + throw new VerificationException( |
| 441 | + "Lookup index [" + indexName + "] has index mode [" + indexMode + "], expected [" + IndexMode.LOOKUP + "]" |
| 442 | + ); |
| 443 | + } |
| 444 | + String clusterAlias = RemoteClusterAware.parseClusterAlias(indexName); |
| 445 | + // Each cluster should have only one resolution for the lookup index |
| 446 | + if (clustersWithResolvedIndices.containsKey(clusterAlias)) { |
| 447 | + throw new VerificationException( |
| 448 | + "Multiple resolutions for lookup index [" + index + "] " + EsqlCCSUtils.inClusterName(clusterAlias) |
| 449 | + ); |
| 450 | + } else { |
| 451 | + clustersWithResolvedIndices.put(clusterAlias, indexName); |
| 452 | + } |
| 453 | + }); |
| 454 | + |
| 455 | + // These are clusters that are still in the running, we need to have the index on all of them |
| 456 | + Stream<EsqlExecutionInfo.Cluster> clusters = executionInfo.getClusterStates(EsqlExecutionInfo.Cluster.Status.RUNNING); |
| 457 | + // Verify that all active clusters have the lookup index resolved |
| 458 | + clusters.forEach(cluster -> { |
| 459 | + String clusterAlias = cluster.getClusterAlias(); |
| 460 | + if (clustersWithResolvedIndices.containsKey(clusterAlias) == false) { |
| 461 | + // Missing cluster resolution |
| 462 | + VerificationException error = new VerificationException( |
| 463 | + "Lookup index [" + index + "] is not available " + EsqlCCSUtils.inClusterName(clusterAlias) |
| 464 | + ); |
| 465 | + // For now, local cluster can not be skipped, so we throw an error |
| 466 | + if (RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY.equals(clusterAlias) |
| 467 | + || executionInfo.isSkipUnavailable(clusterAlias) == false) { |
| 468 | + throw error; |
| 469 | + } else { |
| 470 | + // If we can, skip the cluster and mark it as such |
| 471 | + EsqlCCSUtils.markClusterWithFinalStateAndNoShards( |
| 472 | + executionInfo, |
| 473 | + clusterAlias, |
| 474 | + EsqlExecutionInfo.Cluster.Status.SKIPPED, |
| 475 | + error |
| 476 | + ); |
| 477 | + } |
| 478 | + } |
| 479 | + }); |
| 480 | + |
| 481 | + if (clustersWithResolvedIndices.size() > 1) { |
| 482 | + // If we have multiple resolutions for the lookup index, we need to only leave the local resolution |
| 483 | + String localIndexName = clustersWithResolvedIndices.get(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY); |
| 484 | + if (localIndexName == null) { |
| 485 | + // Get the first index name instead |
| 486 | + localIndexName = clustersWithResolvedIndices.values().iterator().next(); |
| 487 | + } |
| 488 | + var localIndex = new EsIndex(index, newIndexResolution.get().mapping(), Map.of(localIndexName, IndexMode.LOOKUP)); |
| 489 | + newIndexResolution = IndexResolution.valid( |
| 490 | + localIndex, |
| 491 | + localIndex.concreteIndices(), |
| 492 | + newIndexResolution.getUnavailableShards(), |
| 493 | + newIndexResolution.unavailableClusters() |
| 494 | + ); |
| 495 | + } |
| 496 | + |
| 497 | + return result.addLookupIndexResolution(index, newIndexResolution); |
402 | 498 | } |
403 | 499 |
|
404 | 500 | private void initializeClusterData(List<IndexPattern> indices, EsqlExecutionInfo executionInfo) { |
|
0 commit comments