Skip to content

[FEATURE] Report progress.fraction_done for async Calcite PPL queries - #19

Open
penghuo wants to merge 3 commits into
refactor/async-ppl-service-retentionfrom
feat/async-ppl-progress
Open

penghuo wants to merge 3 commits into
refactor/async-ppl-service-retentionfrom
feat/async-ppl-progress

Conversation

@penghuo

@penghuo penghuo commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Description

Adds bounded, monotonic progress.fraction_done to RUNNING asynchronous Calcite PPL submit responses and retained-job GET responses. Partial-result publication remains a separate feature. Implements OpenSearch SQL opensearch-project#5797, using the progress tracker design in §8 of the detailed design on feat/ppl-progress-execution-v0 at 3b7e369.

This PR targets refactor/async-ppl-service-retention, the branch for upstream PR opensearch-project#5832 at 56e18bf67. Its current API and query-job lifecycle define the submit, poll, retention, and error behavior. A submission that completes within its wait budget returns the existing final rows or explain response without a progress object. Retained responses include progress alongside the existing job fields. Immediate failures retain the existing exception response. Retained failures carry the structured synchronous error object introduced by the updated base, alongside their frozen progress value; both survive owner-node forwarding.

OpenSearchQueryJobService creates a distinct tracker for each published job. QueryJob owns the observer, creates the progress-aware execution listeners, samples status, and freezes progress at failure/cancellation. PPLQueryRunner receives a neutral ResultListeners factory, forwards its listeners unchanged to PPLService, and handles final-result conversion; it has no progress tracker, progress getter, or progress-listener implementation.

Progress contract

The following values apply to retained jobs. A submit that finishes within wait_for_completion_timeout returns the normal final response without progress or a polling ID.

State Published fraction
Running Finite, monotonic, within [0.0, 0.8]
Succeeded Exactly 1.0
Failed or internally cancelled Frozen observed progress, never below an already-published value

Sources are registered from the final physical plan and sealed before progress is published. Each scan position has its own identity, including shared plan nodes in self-joins; generated bindings remain stable across repeated enumeration and PIT index pruning.

Single-request work uses shard query-phase callbacks. Paged searches use completed pages plus bounded in-flight progress; Composite aggregations use bucket document coverage. Bounded, best-effort primary document statistics provide shard and source weights. Missing estimates use the design's fallback calculations. This adds no forced track_total_hits requirement.

Coordinator limits complete their underlying sources when their output quota is reached, while other query work can continue. A scoped, core-neutral PhysicalPlanHook applies the equivalent ProgressAwareEnumerableLimit after physical plan selection and before code generation and source registration. The node retains its inputs, offset, fetch, traits, and cost model and delegates row production to Calcite. Closing during exception or cancellation cleanup does not complete abandoned work.

Compatibility

Synchronous and Spark response shapes are preserved. Unobserved scans keep their original generated scan() call, preserving extended explain output. Observed search requests preserve their parent task. The async core DTO retains the typed explain response introduced by opensearch-project#5832; formatting remains in the transport layer, without a new async-core dependency on protocol.

Retained queries with no observed OpenSearch source report 0.0 while running and 1.0 on success. Progress remains an estimate of source work rather than filter selectivity or coordinator CPU work.

Scope

66 files: 42 production, 23 test/support, and 1 documentation. The job-boundary refactor changes four existing production files and six existing test files, with no new repository file. Relative to the previous PR revision, OpenSearchQueryJobService.java and five existing tests now appear in the diff; QueryJobProgressTest was already included. The runner contract now accepts a neutral listener factory, and the tests adapt its callers and verify per-job isolation and listener forwarding.

The source calculator, search callbacks, source registration, coordinator-limit instrumentation, job lifecycle, and response formatting each implement part of issue opensearch-project#5797 and design §8. Scope cleanup removes an unused response helper, a write-only channel field, producer-less page events, unused response flags, and a duplicate test harness. The production planner/code-generation regression and meaningful lifecycle/source/REST coverage remain.

Validation

Implementation and local validation use retention commit 56e18bf67. The target branch has since advanced to 8cb623f23 through four test-only commits; those commits are not incorporated into this reviewed head. The reviewed local snapshot was committed unchanged as 1ec06238f; all ten modified-file hashes match the tested snapshot. Unit counts below come from fresh per-class XML across six modules. Progress and lifecycle IT XML reports were archived independently; discovered-suite registries are excluded.

Check Result
spotlessCheck and git diff --check Passed for the snapshot committed as 1ec06238f
Unit tests: core, opensearch, ppl, protocol, async-query, plugin 9,614 passed; 46 existing skips; zero failures/errors for the 1ec06238f snapshot
Core/PPL/OpenSearch checks and dependent compilation Passed; async-query-core was compiled with the updated core contract
Protocol, async-query and plugin checks Passed for the 1ec06238f snapshot
JaCoCo verification Protocol's 100% line/branch gate, async-query's per-class 100% line/branch gate with existing exclusions, and PPL's configured 90% gate passed. Core/OpenSearch retain their existing 0.0 thresholds. This does not claim 100% overall async-query module coverage
AsyncPPLProgressIT, Calcite enabled with 40-row/bucket pages 21 passed; zero skips/failures/errors for the 1ec06238f snapshot, including inline rows/explain omission and retained-job progress
AsyncPPLQueryLifecycleIT, including sync/inline/retained error parity 19 passed; zero skips/failures/errors for the 1ec06238f snapshot
async-query-core unit suite Last run on 0c40cd5cf; 203 tests. No fresh unit rerun claimed for that module
CalciteExplainIT Last run on f1f112cf6: 262 passed, 4 skipped; no expected-fixture changes
AsyncPPLMultiNodeRoutingIT Last run on f1f112cf6: 4 passed
Calculator comparison against f1f112cf6 2,000 seeded multi-source sequences and 120,578 bit-identical snapshots on 0c40cd5cf; calculator unchanged in 1ec06238f
GitHub CI Triggered by the push of 1ec06238f; checks queued/running
Doctest Last attempted on the inline-response revision; bootstrap failed before parsing docs because its Python 3.7 environment could not install a dependency requiring Python 3.9 or later. Not rerun for this job-boundary refactor

New boundary regressions submit two jobs through the actual service, register the same source id in both, and prove that one job's events do not advance the other. A wiring regression emits events through the observer carried by the job-produced listener and checks the same job's GET progress. PPL tests assert that both row and explain listener instances reach PPLService unchanged and cover explain success/failure. Existing lifecycle progress races, high-water-mark freeze, null/throw fallback, and timeout behavior remain covered.

ProgressEnvelope follows the neighbouring ErrorFormatter's Lombok @UtilityClass form so it introduces no uninstantiable-constructor coverage gap, and AsyncQueryResultResponseFormatterTest covers the progress-present branch for succeeded, running, and failed responses as well as the Spark omission. New coverage includes calculator bounds and weighted combinations, terminal-transition races, parent-task and shard-listener wiring, scan identity, inline progress omission, retained response formatting, and cleanup behavior. CoordinatorLimitProgressEngineTest exercises the production planner, code generator, registrar, and progress signal and asserts 0.4 before the outer search begins; removing the hook causes that assertion to fail at 0.00016. The production engine test also exercises the physical-plan hook through the real statement preparation path. Zero-fetch behavior is covered at the limit wrapper; the existing synchronous planner failure for PPL head 0 is outside this change.

Check List

  • New functionality includes testing.
  • New functionality has Javadoc and user documentation.
  • Commits are signed per the DCO using --signoff.
  • New PPL command checklist — not applicable.
  • API specification companion PR — follow-up.
  • Public documentation issue or PR — follow-up.

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

@penghuo
penghuo requested a review from dai-chen as a code owner October 2, 2026 18:38
@penghuo
penghuo force-pushed the feat/async-ppl-progress branch 2 times, most recently from 990091f to 58fe294 Compare October 2, 2026 19:20
@penghuo penghuo added the enhancement New feature or request label Oct 2, 2026
@penghuo
penghuo force-pushed the feat/async-ppl-progress branch 3 times, most recently from 1d485f2 to f1f112c Compare October 6, 2026 18:57
Report bounded, monotonic progress for every asynchronous Calcite PPL query,
independently of whether partial rows are available. Implements opensearch-project#5797 on top of
the async submit/poll lifecycle from opensearch-project#5832.

Every asynchronous response — submit, poll, and terminal — carries:

    "progress": { "fraction_done": 0.44 }

Guarantees: finite and within [0.0, 1.0]; never decreasing; at most 0.8 while
RUNNING; exactly 1.0 on SUCCEEDED; and the last running value retained on
failure or cancellation. The 20% reserve is not an estimate of coordinator
progress — it exists so that draining every source cannot report a result that
is not ready, because the coordinator may still be sorting, joining, or
reducing.

Progress is derived from signals OpenSearch already produces, so it needs
neither track_total_hits nor partial results:

- per-shard query-phase completion via SearchProgressListener, weighted by
  live primary document counts from one bounded _stats lookup per index
  expression;
- page counts for point-in-time paged scans;
- bucket doc_count coverage for Composite aggregations;
- document-count-weighted combination across sources, falling back to equal
  weight when any source's estimate is unavailable.

Source identity is the physical plan position, claimed during code generation
and baked into each generated scan call. Calcite canonicalizes equal plan
nodes, so a same-index equijoin has both of its scan positions pointing at one
node object; keying on the node would report the whole plan done when the first
of two reads finished. Positions are enumerated from the physical plan via
PLAN_BEFORE_IMPLEMENTATION — which also makes scans inside subquery
expressions visible — and the source set is sealed before anything is
published, so the denominator never grows mid-query.

A consumer that closes a source early (head, a coordinator-side take) completes
it, but only once result draining has succeeded. A bare close is ambiguous:
Linq4j closes a source from its own finally block, which runs identically
whether the query is finishing or failing, and before the exception reaches the
engine. Confirming at end of draining means an aborted query never rounds its
abandoned sources up to done.

Synchronous responses and the Spark async-query responses are unchanged and
carry no progress field. The search-request wrapper that attaches the progress
listener preserves the parent task, so cancellation and resource accounting are
unaffected.

Signed-off-by: Peng Huo <penghuo@gmail.com>
@penghuo
penghuo force-pushed the feat/async-ppl-progress branch from f1f112c to 0c40cd5 Compare October 6, 2026 19:31
Signed-off-by: Peng Huo <penghuo@gmail.com>
Signed-off-by: Peng Huo <penghuo@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant