Repository navigation
[Feature] Resolve cross-cluster index schemas through the remote cluster's GetMappings (#4551) - #5844
Conversation
PR Reviewer Guide 🔍(Review updated until commit fd07eff)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to fd07eff Explore these optional code suggestions:
|
2cba009 to
2a44d92
Compare
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 2a44d92.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
|
Persistent review updated to latest commit 2a44d92 |
2a44d92 to
734ced6
Compare
|
Persistent review updated to latest commit 734ced6 |
Codecov Report❌ Patch coverage is ❌ Your project check has failed because the head coverage (63.78%) is below the target coverage (99.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #5844 +/- ##
============================================
+ Coverage 63.66% 63.78% +0.11%
- Complexity 9022 9063 +41
============================================
Files 959 962 +3
Lines 40836 40974 +138
Branches 4635 4653 +18
============================================
+ Hits 26000 26136 +136
+ Misses 13964 13962 -2
- Partials 872 876 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
734ced6 to
4b345ec
Compare
_field_caps (#4551)GetMappings (#4551)
|
Persistent review updated to latest commit 4b345ec |
GetMappings (#4551)GetMappings (#4551)
| static IndexNames of(String... indexExpression) { | ||
| if (Arrays.stream(indexExpression) | ||
| .noneMatch( | ||
| name -> name.indexOf(RemoteClusterAware.REMOTE_CLUSTER_INDEX_SEPARATOR) >= 0)) { |
There was a problem hiding this comment.
Does TimeBound ":" is removed from index names before call indexNames? cc: @ahkcs
There was a problem hiding this comment.
Yes. Since #5852 the bounds are a single <field,start,end> suffix on the last source, and OpenSearchStorageEngine.getTable strips it with TimeBounds.decode before the name reaches the client.
There was a problem hiding this comment.
Thanks @ahkcs for confirming. TimeBounds.decode in getTable strips the suffix before the name reaches the client, so only real cluster:index names are grouped.
| try { | ||
| return part.apply(remoteMetadata(names.remote())); | ||
| } catch (EmptyResultException e) { | ||
| if (names.local().length == 0) { |
There was a problem hiding this comment.
A skipped remote in a subsearch empties the whole query. With dead unreachable and
skip_unavailable=true:
source=bank,dead:logs-* | head 3→ 3 rows + warningsource=bank | head 3 | append [ source=dead:logs-* ]→ 200, no columns, no rowssource=bank | head 3 | lookup dead:lk account_number→ same
This check runs per relation, but QueryService replaces the whole response with an empty one.
There was a problem hiding this comment.
EmptyResultException is removed. Thanks for the catch.
When every remote in a source is skipped and no local index is named, schema resolution now fails with 503 Remote cluster [dead] is unavailable and was skipped (skip_unavailable is true), so append [ source=dead:logs-* ] and lookup dead:lk … return that error instead of an empty response. A skipped remote next to a local index (source=bank,dead:logs-*) still returns the local rows, covered by testSkippedRemoteWithLocalIndexReturnsTheLocalRows.
| RemoteClusters.get() | ||
| .groupIndices(DEFAULT_INDICES_OPTIONS, indexExpression, index -> false) | ||
| .get(RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY); | ||
| Set<String> local = localGroup == null ? Set.of() : Set.of(localGroup.indices()); |
There was a problem hiding this comment.
Set.of throws on duplicates, and groupIndices keeps them.
source=bank,bank,remote:bank | stats count() returns 14 on main and 400 duplicate element: bank here.
Use Set.copyOf or a HashSet.
There was a problem hiding this comment.
Fixed by the regrouping: names now come straight from groupIndices, with no Set, so source=bank,bank,remote:bank works as on main. A unit test covers a repeated local name next to a remote one (get_index_mappings_accepts_a_repeated_local_name_with_remote_names).
|
|
||
| private static final Cache<List<Object>, RemoteMetadata> CACHE = | ||
| CacheBuilder.newBuilder() | ||
| .maximumWeight(MAX_TOTAL_FIELDS) |
There was a problem hiding this comment.
Entries weighing more than ~25,000 (Indices × 1,000 fields) are never cached: Guava splits maximumWeight across 4 segments and evicts any entry heavier than one segment right after loading it. In a unit test with N distinct 1,000-field mappings read twice, 24 indices take 1 fetch and 26 take 2. Set
concurrencyLevel(1).
There was a problem hiding this comment.
Done, thanks. concurrencyLevel(1), plus largeSchemaWithManyDistinctMappingsIsCached (30 distinct 1,000-field mappings read twice, one fetch). The test fails without the fix.
…appings (opensearch-project#4551) PPL removed the "cluster:" prefix from a cross-cluster index name and read its mapping from the local cluster, which cannot see remote clusters (OpenSearch#6573). A query such as source=remote:logs-* failed with "no such index" unless the same index existed locally. The node client now groups index names by cluster, the same way search does, and reads the mapping and settings of remote indices from each remote cluster with its own client. Local indices are unchanged, and callers no longer split or strip names. RemoteClusterService is passed into the client through a provider that the plugin creates and TransportPPLQueryAction fills at startup. Errors match a search: a missing index, an unreachable remote, and a missing permission each report their own error, and skip_unavailable is applied per cluster. If every remote cluster in a source is skipped and no local index is named, there are no fields to query, so the query fails and names the skipped clusters. Remote schemas are cached for 60 seconds per user, so field-level security is respected. Resolves opensearch-project#4551 Signed-off-by: Ajimelec Gonzalez <ajimelec@amazon.com>
4b345ec to
fd07eff
Compare
|
Persistent review updated to latest commit fd07eff |
Description
PPL queries on a remote cluster index (
source=remote:logs-*) failed withno such index [logs-*], even though the same DSL query (_search) worked. PPL looked for the index's fields on the local cluster, which can't see remote clusters (opensearch-project/OpenSearch#6573). The only workaround was creating an index with the same name and fields on the local cluster.With this change, PPL reads the fields of a remote index from the remote cluster itself, the same way it does for a local index. No local copy is needed, and local queries work as before.
What users get
Queries on
remote:index,*:index, and mixed local and remote indices work in PPL and Log Explorer.These cases return the same result as
_search(the skipped-cluster warning arrives with Warn when a cross-cluster search skips a remote cluster #5851):_searchremote:nope)no such index [nope]no such index [nope]skip_unavailable: falseUnable to open any proxy connections to remote cluster [remote]skip_unavailable: true_clusters.skipped: 1The skipped-cluster warning comes from Warn when a cross-cluster search skips a remote cluster #5851, which reads
_clusters.skippedfrom the search response. Until Warn when a cross-cluster search skips a remote cluster #5851 merges, the rows are correct but nothing says a cluster was skipped.A remote index's fields are cached for 60 seconds per user, so after the first query only the search itself goes to the remote cluster.
How it works:
getIndexMappingsandgetIndexMaxResultWindowsin the node client group names by cluster (withRemoteClusterService.groupIndices, as search does), read each remote cluster with its own client, and merge the results. Callers don't change.RemoteClusterServiceis passed intoOpenSearchNodeClientas a supplier:createComponentsruns before anyTransportServiceexists, so the plugin returns a provider as a component andTransportPPLQueryActionfills it at startup.Permissions: the remote role needs the same permissions as PPL on a local index (each tested by removing it):
indices:admin/shards/search_shardsindices:data/read/searchindices:admin/mappings/getindices:monitor/settings/getTwo more are optional,
indices:admin/resolve/indexandindices:data/read/field_caps*. They let Log Explorer time ranges skip remote indices outside the range.Known differences from
_search_searchskip_unavailable: true, no local indexRemote cluster [remote] is unavailable and was skipped (skip_unavailable is true)(there are no fields to build the query from)_clusters.skipped: 1remote:nope-*)no such index [remote:nope-*], as PPL already does for local indices (#4342)typo:logs-*)no such index [typo:logs-*](treated as a local name, as in search)mappings/getorsettings/getno permissions for [indices:admin/mappings/get] ..., as PPL on a local indexTimeout waiting for taskFailed to fetch data from the index ... Unable to open any proxy connections ...)Unable to open any proxy connections ...The local and remote clusters should also run the same version, because parts of a query can run on the remote cluster.
Docs:
cross_cluster_search.mdremoves the old requirement for a local copy of each remote index, and explains what happens when a remote cluster is down and which permissions the remote role needs.Testing
Unit tests:
RemoteMappingsReaderTest: reading each remote cluster and prefixing its index names, the remotemax_result_window, parsing identical mappings once, the per-user cache (including a large schema of many distinct mappings), skipping vs. failing an unreachable cluster, not caching answers with a skipped cluster, every cluster skipped and its message, and missing index, permission, and empty-pattern errors.OpenSearchNodeClientTest: local-only names never touch the remote service, local and remote mappings and settings merged, every remote skipped with and without local indices, a repeated name, and a date-math name with:staying local.OpenSearchDescribeIndexRequestTest:cluster:indexnames reach the client unchanged.Integration tests, on two clusters with security, on both engines (each class has a Calcite variant), using indices that exist only on the remote:
CrossClusterRemoteSchemaIT: remote-only index rows and aggregation, mixed local and remote indices, missing remote index, unreachable remote with and withoutskip_unavailable, a skipped remote next to a local index, and the cache not sharing one user's fields with another.CrossClusterRemoteSchemaCoverageIT: Log Explorer requests and time bounds over rollover indices, wildcards and*:index, type conflicts, nested fields, grouping by a text field's.keyword, mixed field types, SQL,describe,_explain, and a missing mapping permission.CrossClusterSearchIT: the old "fails without a local mapping" test now checks an index missing on the remote.Related Issues
Resolves #4551
Related: opensearch-project/OpenSearch#6573
Check List
--signoffor-s.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.