8000 fix(api): refuse a bucket-metric buckets_path that reaches no number by amartya1523 · Pull Request #2997 · xerj-org/xerj · GitHub
[go: up one dir, main page]

Skip to content

fix(api): refuse a bucket-metric buckets_path that reaches no number - #2997

Open
amartya1523 wants to merge 5 commits into
xerj-org:mainfrom
amartya1523:fix/bucket-metric-paths
Open

amartya1523 wants to merge 5 commits into
xerj-org:mainfrom
amartya1523:fix/bucket-metric-paths

Conversation

@amartya1523
Copy link
Copy Markdown
Contributor

AI-assisted change, run and reviewed by @amartya1523.

What this changes, and why

A bucket-metric pipeline (max_bucket, avg_bucket, sum_bucket, stats_bucket, ...) reads one number per bucket of the first aggregation in its buckets_path. It gets that number by resolving the rest of the path inside each bucket (BucketHelpers.resolveBucketValue). When the rest names something that isn't a number, ES fails the request:

buckets_path (under the_terms: terms) ES reason
the_terms>the_terms_2>the_max (path goes on below a second terms) buckets_path must reference either a number value or a single value numeric metric aggregation, got: [Object[]] at aggregation [the_terms_2]
the_terms>the_terms_2 (path stops at a terms on an integer) ..., got: [LongTerms] at aggregation [the_terms_2]
the_terms>the_percentiles (percents: [10, 50, 90, 99], tdigest or hdr) ..., but [the_percentiles] contains multiple values. Please specify which to use.

XERJ skipped every bucket and answered 200 with "the_bad_max": {"value": null}, which is indistinguishable from "no data". Four catch-lenient.txt lines are these cases (aggregations/pipeline.yml):

  • Max pipeline through terms agg
  • Max pipeline on terms agg
  • Max pipeline on percentiles tdigest agg without specifying percent
  • Max pipeline on percentiles hdr agg without specifying percent

A fifth entry turned out to be a runner bug, not an engine gap. Max pipeline on percentiles with incorrect name expects No aggregation [unknown_aggregation] found for path [the_terms>unknown_aggregation];. XERJ already returns exactly that (find_invalid_pipeline_agg, #1824). The YAML pattern escapes the > as \>, which Java's regex reads as a literal >. The runner compiles patterns with Rust's regex crate, where \< / \> are start-/end-of-word assertions, so the pattern could never match any body.

Fix:

  • es_compat.rs: new find_bucket_metric_path_error. It runs in search_impl next to find_unsupported_metric_agg_field, where the target indices' mappings are at hand. For every sibling pipeline with a string buckets_path, it takes the first element (it must be a multi-bucket aggregation, otherwise the path is left alone) and walks the rest:

    • multi-bucket aggregation, path continues → got: [Object[]] at aggregation [x]
    • multi-bucket aggregation, path ends → got: [<class>] at aggregation [x]. A terms result is named by the field's mapped type in the first target index: LongTerms (integer types, date, boolean), DoubleTerms (floating types), StringTerms (keyword, ip, ...). histogram, date_histogram, range, date_range and filters map to their Internal* classes. Anything else isn't classified.
    • multi-value metric, path ends (stats, extended_stats, boxplot, and percentiles / percentile_ranks with more than one value) → ... but [x] contains multiple values. Please specify which to use. A percentiles with exactly one percent is single-valued (ES 8.4+), and the YAML's passing percents: [99] cases depend on that.
    • single-bucket aggregation (filter, nested, ...) is walked into.
    • left to the executor, as before: a value key (p.99, p[99.9], st.max), a bucket property (_count, _key, ...), a single-value metric, and anything unclassified.

    The error is ES's aggregation_execution_exception root cause inside a 400 search_phase_execution_exception.

  • tests/es-compat-yaml/src/main.rs: new java_regex_to_rust. It rewrites \< / \> to the literal characters before compiling a catch: pattern. Other escapes, including \\, pass through.

Evidence

Tests run against main first:

# main 116d5a676
test es_compat::bucket_metric_path_tests::paths_without_a_number_are_refused ... FAILED
  {... "the_bad_max":{"max_bucket":{"buckets_path":"the_terms>the_terms_2>the_max"}}} -> {"took":1, ... 200
test catch_regex_tests::java_escaped_angle_brackets_match_literally ... FAILED
  left: Some("error body does not match")
 right: None

# this branch
test es_compat::bucket_metric_path_tests::paths_without_a_number_are_refused ... ok
test es_compat::bucket_metric_path_tests::paths_to_a_number_still_answer ... ok
test catch_regex_tests::java_escaped_angle_brackets_match_literally ... ok
  • Refused (asserts the 400, the root-cause type and ES's reason):
    • the four YAML requests (tdigest and hdr)
    • a terms end on a keyword (StringTerms) and on a double (DoubleTerms)
    • percentiles with default percents reached through a filter
  • Still answers, with the right max value:
    • the_terms>m (max)
    • the_terms>p (single percent)
    • the_terms>p.99
    • the_terms>st.max
  • Runner: the exact pipeline.yml pattern matches XERJ's message, and \\> stays an escaped backslash.
  • Whole suite: I replayed the new rule (in Python, same logic) over every search body in the ES-YAML suite. It fires on exactly these four catch: requests and on no request that is meant to succeed. Every sibling buckets_path in avg_bucket.yml, sum_bucket.yml, percentiles_bucket.yml, stats_bucket.yml, typed_keys.yml, rare_terms.yml, etc. is unaffected.

Checks

  • cargo fmt -p xerj-api -p es-yaml-runner -- --check
  • cargo test --profile quick -p xerj-api --lib: 432 passed, 0 failed
  • cargo test --profile quick -p es-yaml-runner: 1 passed, 0 failed
  • YAML: all five pipeline.yml cases now enforce their expectation; their catch-lenient lines are deleted. No other catch: pattern in the suite uses \< or \>, so the runner change can't change another case.
  • Docs: none describe the old behaviour.

Not run locally:

  • The ES-YAML suite against a live node, --release builds, clippy and the integration binaries. Disk on this machine is at 99%; CI runs all of these.

Provenance

  • Written by: AI-assisted, run and reviewed by @amartya1523
  • Verified: the test outputs and the suite-wide replay above, on macOS 26 (aarch64), quick profile, based on main 116d5a676.
  • Assumed / found along the way:
    • The class names follow ES's result types; only Object[] and LongTerms are pinned by the YAML.
    • ES finds these at reduce time, so with zero buckets it doesn't error. This check runs on the request, so it also refuses the path when no document matches. No YAML case has that shape, and the path is equally meaningless there.
    • Only _search is covered; _msearch has its own path.
    • Separate engine gaps, not changed here: the executor returns null today for three valid sibling paths: the_terms>_count, the_terms>t2._bucket_count and a path through a single-bucket agg (the_terms>f>m). They're out of this PR's scope (no YAML case covers them); I can take them as a follow-up.

max_bucket and its siblings read one number per bucket of the first
aggregation in buckets_path, by resolving the rest of the path inside
each bucket. When that rest names another multi-bucket aggregation, or
a multi-value metric without saying which value, ES fails with
"buckets_path must reference either a number value or a single value
numeric metric aggregation, got: [LongTerms] at aggregation [x]" (or
"... but [x] contains multiple values. Please specify which to use.").
XERJ skipped every bucket and answered 200 with a null value.

_search now classifies each sibling pipeline's path before running:
Object[] when the path goes on below a multi-bucket aggregation, the
result class (LongTerms/DoubleTerms/StringTerms by field type, the
histogram and range classes) when it stops at one, and the
multiple-values error for stats, extended_stats, boxplot and
percentiles or percentile_ranks with more than one value. Value keys,
bucket properties and anything it cannot classify are left alone.

The YAML runner now reads Java's \< and \> escapes as the literal
characters they are in ES's patterns; Rust's regex took them as word
boundaries, so pipeline.yml's "incorrect name" case, whose message
XERJ already returns, could never match.

Retires the five aggregations/pipeline.yml catch-lenient entries.
API: nested terms, terms ends (long, keyword, double), and percentiles with several percents (tdigest, hdr, through a filter) are a 400 with ES's reason; metric, single-percent and value-key paths still answer with the value. Runner: the pipeline.yml incorrect-name pattern matches ES's message.
@cla-bot cla-bot Bot added the cla-signed label Nov 6, 2026
…nner

clippy's items_after_test_module rejects a test module ahead of other items.
Build + Test failed only on xerj-cluster's log_catchup::restart_replays_the_committed_index_and_in_sync_set timing out; this PR does not touch the cluster crate.
@amartya1523
Copy link
Copy Markdown
Contributor Author

Status note on the red Build + Test: everything from this PR passes (ES-YAML conformance, Format + Clippy, and the xerj-api lib suite, 432/432 locally on the branch merged with main 34bf35b). The two Build + Test failures since the merge were both cluster timing tests that this PR does not touch:

  • e88d392 / eb2a2ed: xerj-cluster log_catchup::restart_replays_the_committed_index_and_in_sync_set (timed out waiting for a follower)
    • a5f3a6e: xerj-api cluster_metadata_replicates::a_late_member_copies_documents_written_before_it_joined (503 replica_not_acknowledged, timed out waiting for the metadata change)
      A re-run of that job should clear it; I can't re-run single jobs from the fork.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0