Repository navigation
[Feature][PPL] Report progress for asynchronous Calcite queries - #5910
Conversation
Add progress.fraction_done to async submit-timeout and GET responses. QueryRunner exposes a best-effort estimate and QueryJob keeps it monotonic, below 1.0 while running, 1.0 on success, and the last running value on failure or cancellation. Each Calcite index scan registers with a per-query tracker held by the async PPLQueryTask. Searches report shard completion through a SearchProgressListener; PIT and composite pages report units read over the response's total hits, so no exact hit count or _stats call is needed. Synchronous queries create no tracker and are unchanged. Resolves opensearch-project#5797 Signed-off-by: Kai Huang <ahkcs@amazon.com>
PR Reviewer Guide 🔍(Review updated until commit ce8f7a9)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to ce8f7a9 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit b605dc5
Suggestions up to commit 18525b1
Suggestions up to commit ac83d61
Suggestions up to commit c6225d0
Suggestions up to commit 4841c12
|
- Do not mark a scan complete on close, so a failed query keeps its last running progress instead of jumping to the cap. - Start a fresh estimate on each pass of a re-iterated scan. - Clamp a RUNNING response below 1.0 when the job succeeds between the wait expiring and the response being built. - Hold source progress on the scan instead of keying the tracker by RelNode, so retained jobs do not keep their plan alive. - Carry the search pipeline onto the tracked SearchRequest copy. - Skip sampling runner progress once the job is terminal. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit ceb4b52 |
- Register a scan the planner shares across the plan as one source. - Measure composite progress in buckets over the row limit when total hits is only a lower bound, instead of a growing document estimate. - Return the result when a job succeeds after the wait budget expires. - Remove the unused QueryJobStatus constructor that defaulted progress. - Document that queries without a Calcite index scan report 0.0. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 6b55e7f |
- Ask for an exact total hit count on the first page of an async PIT or composite scan. Without it, scans whose size limit is not pushed down (e.g. under streamstats or stats) reported k/(k+1) for PIT and ~0 for composite on indices over 10,000 documents. - Give each use of a scan the planner shares (e.g. a self-join) its own source; re-scans reuse the last one. - Mark a scan complete when closed early, and stop sampling progress at the failure transition so FAILED keeps the last polled value. - Report the outcome of a job that finished after the wait budget expired, for failure as well as success. - Always pass progress to OpenSearchClient.search and read sources from a CopyOnWriteArrayList. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 825591a |
- Keep the first page's total once recorded; later pages report the default lower bound and were clearing it. - Count PIT total hits only up to the row limit, so the count costs no more than the rows the scan reads; composite keeps an exact count. - Do not mark a scan complete on close again: a query falling back to another engine closes its scans while still running. - Pass each implemented scan position its own progress source through the generated code instead of holding sources on the RelNode. - Sample RUNNING progress from a delayed task instead of a placeholder result, and simplify the tracker mean and the estimate. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit b15808c |
Codecov Report❌ Patch coverage is ❌ Your project check has failed because the head coverage (75.51%) 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 #5910 +/- ##
============================================
+ Coverage 75.38% 75.51% +0.13%
- Complexity 10385 10439 +54
============================================
Files 962 969 +7
Lines 41087 41306 +219
Branches 4682 4711 +29
============================================
+ Hits 30973 31193 +220
+ Misses 8806 8788 -18
- Partials 1308 1325 +17
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:
|
- Report progress for a plan whose root is a single scan. Such a plan runs without generated code, so its scan registered no source and plain or fully pushed queries reported 0.0 until done. Add Scannable.scanAsRoot for that path. - Restore a cancellable wait timer so an early outcome does not keep the job and its result reachable until the budget expires. - Never fail a search because progress tracking threw. - Remove the test-only Running(id) constructor, and document fallback and graphlookup behavior. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 4841c12 |
- Hold each scan pass's estimate in its own state object and bind every tracked search to its pass, so a late prefetch response from an earlier pass cannot change the current one. - Count PIT total hits exactly only when the scan has no row limit and reads every match anyway; otherwise keep the default threshold, so the first page costs the same as a synchronous scan. - Start a new pass on enumerator reset, and let a profiled root scan register its progress. - Render fraction_done on the submit body as a double, matching GET. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit c6225d0 |
Remove the composite bucket fallback, which an exact first-page total makes unreachable, and constructor overloads only tests used. Shorten comments to what the code does not say. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit ac83d61 |
penghuo
left a comment
There was a problem hiding this comment.
Did we test cross-cluster cases
| * expires before the job finishes. Inline success and failure remove the job before the returned | ||
| * stage completes. Retention is attached only when a {@code Running} result exposes an id for | ||
| * polling. |
Cover a remote root scan, a remote blocking scan with no row limit, a remote aggregation, and a mixed local and remote source: progress stays bounded and monotonic, and async rows equal sync rows. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 18525b1 |
Remote single-request searches and composite pages run under ccs_minimize_roundtrips, so the local listener sees no remote shards and they report progress only when the response arrives. Document it and cover a remote single-request aggregation. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit b605dc5 |
Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit ce8f7a9 |
Description
Adds
progress.fraction_doneto asynchronous Calcite PPL responses: the submit response whenwait_for_completion_timeoutexpires, andGET /_plugins/_async_query/{id}.{"id": "<opaque-id>", "status": "RUNNING", "progress": {"fraction_done": 0.42}, "schema": [], "datarows": [], "total": 0}Contract (enforced by
QueryJob, not the engine)fraction_donePENDING0.0RUNNING[0.0, 0.95], never decreases between pollsSUCCEEDED1.0FAILED/CANCELLEDSynchronous responses, including async submissions that finish within the wait, keep the existing body without
progress. Spark async-query responses are unchanged.Lifecycle seam (
core/job)QueryProgressrecord andQueryRunner.progress()(defaults to0.0; called from any thread, must not block).QueryJobsamples the runner outside its monitor, then clamps and keeps the maximum. Cancellation freezes the current value. Failure is not sampled, because a failing engine has already closed its scans, soFAILEDkeeps the last value observed while running. A runner that throws fromprogress()keeps the previous value.QueryJobStatusandQueryResult.Runningcarry the progress.Runningsamples it when the wait budget expires, not when the request arrives, through a cancellable timer. A job that finishes before the timer fires returns its outcome inline, success or failure.QueryRunner.progress()is a default method, so other engines need no change.Engine (
opensearch)QueryProgressTrackeris attached toPPLQueryTaskonly for async submissions. Synchronous queries create no tracker; their searches take the same path with no progress attached.CalciteEnumerableIndexScanposition adds a source inimplement()and passes it toscan(source)through the generated code, so all sources are known before the first search runs, a scan the planner shares (e.g. a self-join) gets one source per use, and no state lives on the RelNode. A plan whose root is a single scan runs without generated code; it registers through a newScannable.scanAsRoot()default. Synchronous code generation is unchanged. The tracker holds no plan references, so a retained job does not keep its plan alive. Query progress is the mean of the source progress values.OpenSearchNodeClientattaches aSearchProgressListenerthrough aSearchRequest.createTaskoverride (the same approach as the asynchronous-search plugin). It reuses the task propagation added in fix: Register background search tasks as children of parent #5888 for background searches._statscall._statswould requireindices:monitor/stats, and its index-level count would reveal approximate hidden-document volume under DLS.track_total_hits=true), since its pages visit every matching document anyway. A PIT scan counts exactly only when it has no row limit, i.e. it reads every match anyway (its size limit sits abovestreamstats,stats, …); a PIT scan with a row limit keeps the default threshold, so its first page costs the same as a synchronous one. PIT pages read one snapshot, so one count is enough. Reviewers: this relaxes the issue's "no exact total" wording for async scans only; happy to revisit.doc_countread ÷ total hitsappend,join, …)sort,eventstats, …)Observed progress
AsyncPPLProgressIT, polled every 20 ms (10,000-document fixture,max_result_window=10):RUNNINGstreamstats … | stats max(…)(1,000 PIT pages)SUCCEEDED, 1.0| append [ source=accounts | stats count() ]SUCCEEDED, 1.0fields n(root scan, 1,000 PIT pages)SUCCEEDED, 1.0stats count() by n(single composite page)SUCCEEDED, 1.0evalafter the full scanFAILED, ≥ last running valueTests
AsyncPPLProgressIT(new)AsyncPPLQueryLifecycleIT(existing)CalciteCrossClusterAsyncProgressIT(new; remote scan, remote blocking scan, remotestats by, remote single-requeststats, local + remote source; async rows equal sync)CalciteCrossClusterSearchIT(existing)checkforcore,opensearch,ppl,async-query-core,async-query, and the plugin (unit tests, JaCoCo, Spotless)New unit coverage covers
QueryJobprogress invariants,SourceProgressmath for each shape, the tracker, node-client listener attachment, the background scanner, enumerator completion, the formatter, routing, andPPLQueryRunner.Known limitations
0.0and lowers the mean. Success still reports1.0.0.0→0.95.ccs_minimize_roundtrips, where the remote cluster reduces its own shards, so they report only when the response arrives (0 → done; a single-page remotestats byshows 0 → 0.95). Turning minimize-roundtrips off for async only would change how CCS executes, so it is left as is.graphlookuptraversal scans are not counted; a query that falls back to the legacy engine keeps the value Calcite reached.doc_count; a group-by on a multi-valued field counts a document once per bucket, so that source can reach the cap early.FAILEDkeeps the last value observed while running. A scan is complete only when exhausted or at its row limit, not when closed, because a failing query, or one falling back to the legacy engine, closes its scans too. A scan closed early by a limit that is not pushed down therefore under-reports until success.Related Issues
Resolves #5797
Part of #5765
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.