Skip to content

[Enhancement] Forward cluster planning settings to the Analytics Engine unified query path - #5611

Merged
RyanL1997 merged 4 commits into
opensearch-project:mainfrom
RyanL1997:investigate/ae-plugin-settings
Sep 2, 2026
Merged

[Enhancement] Forward cluster planning settings to the Analytics Engine unified query path#5611
RyanL1997 merged 4 commits into
opensearch-project:mainfrom
RyanL1997:investigate/ae-plugin-settings

Conversation

@RyanL1997

@RyanL1997 RyanL1997 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Description

Several cluster settings were silently ignored on the Analytics Engine (unified query) path — the AE route always planned against UnifiedQueryContext.Builder's hardcoded seed value regardless of the configured cluster value. The default (non-AE) pipeline honored them correctly.

Root cause. RestUnifiedQueryAction.applyClusterOverrides() forwarded only a hand-picked subset of cluster settings into the UnifiedQueryContext, while UnifiedQueryContext.Builder independently seeds a default settings map. Any planning setting present in the seed map but absent from the forward list silently regressed to its default. This is a two-lists-drift defect: the two lists are maintained independently, so the drift is invisible until someone configures the setting and it does nothing.

Settings fixed

Setting Value the AE path actually used Effect
plugins.query.size_limit pinned to 10000 configured result cap ignored
plugins.ppl.pattern.method pinned to SIMPLE_PATTERN configured patterns default ignored
plugins.ppl.pattern.mode pinned to LABEL ditto
plugins.ppl.pattern.max.sample.count pinned to 10 ditto
plugins.ppl.pattern.buffer.limit pinned to 100000 ditto
plugins.ppl.pattern.show.numbered.token pinned to false ditto

Every one of these has a cluster-side default identical to the seeded value, so behavior is unchanged unless an operator explicitly configured the setting — at which point the configured value now takes effect. No query that works today changes behavior on defaults.

Fix

  • Replace the hand-maintained forwardClusterSetting calls with a single FORWARDED_CLUSTER_SETTINGS allow-list — one source of truth for which cluster settings the unified path honors. This matches the existing shape of the sibling handler RestQuerySettingsAction, which declares its own settings allow/deny lists the same way.
  • Add a drift guard (everySeededPlanningSettingIsClassified) asserting that every key the builder seeds is either forwarded or explicitly listed as deliberately excluded, with a reason. Adding a key to the builder's seed map without classifying it now fails a test instead of silently regressing. The guard reads the seeded keys through the existing public Settings#getSettings() API — no reflection, no new production API.

Verified end to end on a live Analytics Engine cluster

Single-node cluster with composite-engine, parquet-data-format, analytics-engine, analytics-backend-datafusion, analytics-backend-lucene + this plugin, querying parquet-backed composite indices. Same cluster, same data, baseline build vs this PR's build — every forwarded key this PR adds:

Setting probed Baseline (setting ignored) This PR (setting honored)
query.size_limit=2 6 rows 2 rows
query.size_limit=4 6 rows 4 rows
pattern.mode=AGGREGATION 6 rows, schema [age, name, patterns_field] 1 row, schema [patterns_field, pattern_count, sample_logs]
pattern.method=BRAIN <*> <*> <*> <*> <*> <*>.<*>.<*>.<*> <*> <*> <*> user <*> logged in from <*IP*> at port <*>
pattern.show.numbered.token=true <*> <*> <*> … <token1> <token2> <token3> …
pattern.max.sample.count=2 / =5 n/a — pattern_count field absent, since pattern.mode was ignored too len(sample_logs) = 2 / 5 respectively

plugins.ppl.pattern.buffer.limit is forwarded by the same mechanism but is not covered by a live-cluster probe: its minimum is 50000, so distinguishing values requires more buffered patterns than a probe dataset produces. It is covered only by the unit test asserting the value reaches the plan context.

Deliberately not forwarded

  • plugins.calcite.enabled — the unified path is Calcite-based by definition and must force it on regardless of the cluster value.
  • plugins.ppl.values.max.limit — forwarding this one makes the AE route strictly worse. The cap is applied by attaching a limit argument to values(), which lowers to array_agg(DISTINCT x, limit); the DataFusion backend has no binding for that two-argument form. Confirmed on the cluster above: with the key forwarded, stats values(f) fails with UnsupportedOperationException: Unable to find binding for call array_agg(DISTINCT $0, $1), surfaced as HTTP 500 — where today it merely ignores the cap. Turning a silent no-op into a hard failure is a regression, so the key stays out until the backend can bind the limited form. Tracked in [BUG] plugins.ppl.values.max.limit cannot be honored on the Analytics Engine route (no binding for array_agg(DISTINCT x, limit)) #5736; the gap is already recorded in-repo as Capability.VALUES_LIMIT_NOT_HONORED.
  • plugins.ppl.subsearch.maxout / plugins.ppl.join.subsearch_maxout — the builder seeds these to 0 (unlimited) on purpose, to keep LogicalSystemLimit out of plans built by external consumers of the unified query API. Note these do diverge from the cluster defaults (10000 / 50000) even when unconfigured, so the AE path currently runs subsearches unbounded. Whether the in-cluster REST path should override that documented choice is a separate behavioral decision, tracked in [BUG] PPL subsearch maxout settings do not apply on the Analytics Engine path (diverges at defaults) #5735.

One further gap found during this work is also out of scope here, tracked in #5734: plugins.calcite.all_join_types.allowed is not seeded, so AstBuilder.validateJoinType reads null and its config != null && !config check skips validation entirely — the high-cost-join guardrail is inactive on the AE path. Restoring it is a user-visible tightening, so it belongs in its own PR.

Check List

  • New functionality includes testing.
  • New functionality has been documented in code.
  • Commits are signed per the DCO using --signoff.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 6e2d9ba)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to ab19073

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Ensure list immutability

The FORWARDED_CLUSTER_SETTINGS list should be immutable to prevent accidental
modification at runtime. Consider using List.copyOf() or wrapping with
Collections.unmodifiableList() to ensure the list cannot be modified after
initialization.

plugin/src/main/java/org/opensearch/sql/plugin/rest/RestUnifiedQueryAction.java [379-389]

 @VisibleForTesting
 static final List<Key> FORWARDED_CLUSTER_SETTINGS =
-    List.of(
+    List.copyOf(List.of(
         Key.QUERY_SIZE_LIMIT,
         Key.PPL_REX_MAX_MATCH_LIMIT,
         Key.PPL_SYNTAX_LEGACY_PREFERRED,
         Key.MAX_EXPRESSION_DEPTH,
         Key.PATTERN_METHOD,
         Key.PATTERN_MODE,
         Key.PATTERN_MAX_SAMPLE_COUNT,
         Key.PATTERN_BUFFER_LIMIT,
-        Key.PATTERN_SHOW_NUMBERED_TOKEN);
+        Key.PATTERN_SHOW_NUMBERED_TOKEN));
Suggestion importance[1-10]: 2

__

Why: List.of() already returns an immutable list in Java 9+, so wrapping it with List.copyOf() is redundant and adds no value. The suggestion is technically correct but unnecessary.

Low

Previous suggestions

Suggestions up to commit 1a401a3
CategorySuggestion                                                                                                                                    Impact
General
Log missing cluster settings

The method silently skips settings when getSettingValue returns null, which could
mask configuration errors or unintended defaults. Consider logging when a setting is
not forwarded, or validate that all expected settings are present before forwarding.

plugin/src/main/java/org/opensearch/sql/plugin/rest/RestUnifiedQueryAction.java [397-401]

 private void forwardClusterSetting(UnifiedQueryContext.Builder builder, Key key) {
   Object value = pluginSettings.getSettingValue(key);
   if (value != null) {
     builder.setting(key.getKeyValue(), value);
+  } else {
+    logger.debug("Cluster setting {} not found, using default value", key.getKeyValue());
   }
 }
Suggestion importance[1-10]: 4

__

Why: While logging missing settings could aid debugging, the current behavior of silently using defaults when getSettingValue returns null is intentional and documented. The suggestion adds marginal value for observability but doesn't address a critical issue.

Low
Suggestions up to commit 506411d
CategorySuggestion                                                                                                                                    Impact
General
Log when settings are not forwarded

The method silently skips settings when getSettingValue returns null, which could
mask configuration errors or unintended defaults. Consider logging when a setting is
not forwarded, or validating that all expected settings are present to ensure
cluster configuration is properly applied.

plugin/src/main/java/org/opensearch/sql/plugin/rest/RestUnifiedQueryAction.java [396-400]

 private void forwardClusterSetting(UnifiedQueryContext.Builder builder, Key key) {
   Object value = pluginSettings.getSettingValue(key);
   if (value != null) {
     builder.setting(key.getKeyValue(), value);
+  } else {
+    logger.debug("Cluster setting {} not forwarded (null value)", key.getKeyValue());
   }
 }
Suggestion importance[1-10]: 4

__

Why: Adding debug logging for null settings could help with troubleshooting, but this is a minor observability improvement. The current behavior of silently skipping null values is intentional and acceptable, as not all settings may be configured. The suggestion assumes a logger field exists without verifying it in the PR diff.

Low
Suggestions up to commit a59cdb7
CategorySuggestion                                                                                                                                    Impact
General
Ensure consistent Integer type return

The mock returns a primitive int (500) but getSettingValue likely returns an Integer
object. Ensure type consistency to avoid potential ClassCastException or autoboxing
issues. Explicitly return Integer.valueOf(500) for clarity.

plugin/src/test/java/org/opensearch/sql/plugin/rest/RestUnifiedQueryActionTest.java [242-244]

 when(pluginSettings.getSettingValue(
         org.opensearch.sql.common.setting.Settings.Key.QUERY_SIZE_LIMIT))
-    .thenReturn(500);
+    .thenReturn(Integer.valueOf(500));
Suggestion importance[1-10]: 3

__

Why: While using Integer.valueOf(500) is more explicit, Java's autoboxing handles the conversion automatically. The suggestion is technically correct but offers minimal practical benefit since autoboxing is standard and reliable in modern Java.

Low
Use Boolean object for consistency

The mock returns a primitive boolean (false) but getSettingValue likely returns a
Boolean object. Use Boolean.FALSE instead of false to ensure type consistency and
avoid potential autoboxing issues.

plugin/src/test/java/org/opensearch/sql/plugin/rest/RestUnifiedQueryActionTest.java [259-261]

 when(pluginSettings.getSettingValue(
         org.opensearch.sql.common.setting.Settings.Key.CALCITE_ENGINE_ENABLED))
-    .thenReturn(false);
+    .thenReturn(Boolean.FALSE);
Suggestion importance[1-10]: 3

__

Why: Similar to the previous suggestion, using Boolean.FALSE instead of false is more explicit but autoboxing handles this automatically. The change provides marginal improvement in code clarity without addressing any actual bug.

Low

@RyanL1997 RyanL1997 added enhancement New feature or request analytic-engine labels Jul 7, 2026
@RyanL1997 RyanL1997 changed the title Forward QUERY_SIZE_LIMIT to the Analytics Engine unified query path [Enhancement] Forward QUERY_SIZE_LIMIT to the Analytics Engine unified query path Jul 7, 2026
@RyanL1997
RyanL1997 force-pushed the investigate/ae-plugin-settings branch from a59cdb7 to 506411d Compare September 1, 2026 18:20
@RyanL1997 RyanL1997 changed the title [Enhancement] Forward QUERY_SIZE_LIMIT to the Analytics Engine unified query path [Enhancement] Forward cluster planning settings to the Analytics Engine unified query path Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 506411d

…ry path

The Analytics Engine (unified query) path silently ignored several cluster
settings, always planning against UnifiedQueryContext.Builder's hardcoded
seed value regardless of what the operator configured. The default
(non-AE) pipeline honored them correctly.

Root cause: RestUnifiedQueryAction.applyClusterOverrides() forwarded only a
hand-picked subset of cluster settings into the UnifiedQueryContext, while
the builder independently seeds a default settings map. Any planning
setting present in the seed map but absent from the forward list regressed
to its default -- a two-lists-drift defect.

Settings fixed (all verified to reach the plan context only with this
change):

  plugins.query.size_limit              pinned to 10000
  plugins.ppl.pattern.method            pinned to SIMPLE_PATTERN
  plugins.ppl.pattern.mode              pinned to LABEL
  plugins.ppl.pattern.max.sample.count  pinned to 10
  plugins.ppl.pattern.buffer.limit      pinned to 100000
  plugins.ppl.pattern.show.numbered.token  pinned to false
  plugins.ppl.values.max.limit          read back null, so the configured
                                        cap on values() never applied

Each has a cluster-side default identical to the seeded one, so behavior is
unchanged unless an operator explicitly configured the setting -- at which
point the configured value now takes effect.

Replaces the hand-maintained forwardClusterSetting calls with a single
FORWARDED_CLUSTER_SETTINGS allow-list, and adds a drift guard
(everySeededPlanningSettingIsClassified) asserting every key the builder
seeds is either forwarded or explicitly documented as excluded, so this
defect cannot recur silently.

Deliberately not forwarded:
  plugins.calcite.enabled -- the unified path is Calcite-based by
    definition and must force it on.
  plugins.ppl.subsearch.maxout / plugins.ppl.join.subsearch_maxout --
    seeded to 0 (unlimited) on purpose to keep LogicalSystemLimit out of
    plans built by external consumers of the unified query API. These do
    diverge from the cluster defaults (10000 / 50000) even when
    unconfigured; whether the in-cluster REST path should override that is
    a separate behavioral decision, tracked in opensearch-project#5735.

Testing: the four new unit tests each fail without this change
(expected:<BRAIN> but was:<SIMPLE_PATTERN>, expected:<100> but was:<null>,
expected:<500> but was:<10000>) and pass with it.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
@RyanL1997
RyanL1997 force-pushed the investigate/ae-plugin-settings branch from 506411d to 1a401a3 Compare September 1, 2026 18:22
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 1a401a3

Verified against a live composite/parquet analytics-engine cluster:
forwarding plugins.ppl.values.max.limit makes the AE route strictly
worse, not better.

The cap is applied by attaching a `limit` argument to the values()
aggregate, which lowers to array_agg(DISTINCT x, limit). The DataFusion
backend has no binding for that two-argument form, so once the setting
actually reaches the parser the query fails outright:

  UnsupportedOperationException: Unable to find binding for call
  array_agg(DISTINCT $0, $1)

served to the client as HTTP 500 "Internal error". Today the same query
merely ignores the cap and returns all values. Turning a silent no-op
into a hard failure is a regression, so the key stays unforwarded until
the backend can bind the limited form -- the gap already tracked by
Capability.VALUES_LIMIT_NOT_HONORED.

The remaining forwarded settings were confirmed end to end on the same
cluster, baseline build vs fixed build:

  plugins.query.size_limit=2         6 rows  -> 2 rows
  plugins.query.size_limit=4         6 rows  -> 4 rows
  plugins.ppl.pattern.mode=AGGREGATION
      baseline: 6 rows, schema [age, name, patterns_field]  (ignored)
      fixed:    1 row,  schema [patterns_field, pattern_count, sample_logs]

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Signed-off-by: Jialiang Liang <ryanleeang@gmail.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 4ca83de

Addresses review feedback on the FORWARDED_CLUSTER_SETTINGS javadoc.

The prose had grown to ~28 lines and, worse, was incomplete: it named
only four exclusions and omitted CALCITE_SUPPORT_ALL_JOIN_TYPES
entirely.

Re-derived the full set by tracing every getSettingValue reachable from
the unified context's Settings -- SysLimit.fromSettings, AstBuilder /
AstExpressionBuilder / AstBuildGuard (the parsers the context builds),
UnresolvedPlanHelper, and CalcitePlanContext. Fourteen keys are read on
that path: nine forwarded, five deliberately not.

  CALCITE_ENGINE_ENABLED         unified path is Calcite by definition
  PPL_SUBSEARCH_MAXOUT           seeded unlimited on purpose (opensearch-project#5735)
  PPL_JOIN_SUBSEARCH_MAXOUT      likewise (opensearch-project#5735)
  PPL_VALUES_MAX_LIMIT           forwarding 500s the route (opensearch-project#5736)
  CALCITE_SUPPORT_ALL_JOIN_TYPES never seeded; guard inactive (opensearch-project#5734)

The javadoc is now a compact bulleted list of those five with an issue
reference each -- shorter than before and, unlike before, complete.

Rather than leaving that claim as prose, DELIBERATELY_NOT_FORWARDED in
the test now carries all five and documentedExclusionsAreNotForwarded
asserts none of them reaches the plan context, using a sentinel value so
"forwarded" is distinguishable from "seeded" and from "absent". Verified
the guard bites: adding PPL_VALUES_MAX_LIMIT to the forward list fails
with "plugins.ppl.values.max.limit must not be forwarded ... Actual:
-12345". This replaces the narrower valuesMaxLimitIsNotForwarded test.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Signed-off-by: Jialiang Liang <ryanleeang@gmail.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit ab19073

…mments

Per review, the production javadoc is cut to the rule plus a pointer:
the per-key exclusion reasons live in one place only, the test's
DELIBERATELY_NOT_FORWARDED, which is also what pins them. Down from ~28
lines originally to 8.

Issue references are removed from code comments in both files; the
reasons stand on their own, and tracking belongs in the PR and issues
rather than in comments that go stale.

Also dropped the hardcoded "five" from the javadoc so the count cannot
rot as the list changes -- the enumerated list is the answer.

Signed-off-by: Jialiang Liang <jiallian@amazon.com>
Signed-off-by: Jialiang Liang <ryanleeang@gmail.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 6e2d9ba

@RyanL1997
RyanL1997 merged commit 797f01a into opensearch-project:main Sep 2, 2026
40 checks passed
@RyanL1997
RyanL1997 deleted the investigate/ae-plugin-settings branch September 2, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

analytic-engine enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants