Skip to content

Fail op chains of terminated MSE queries instead of dropping them - #19854

Merged
yashmayya merged 4 commits into
apache:masterfrom
yashmayya:fail-terminated-mse-op-chains
Oct 16, 2026
Merged

yashmayya merged 4 commits into
apache:masterfrom
yashmayya:fail-terminated-mse-op-chains

Conversation

@yashmayya

@yashmayya yashmayya commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

PR flow

Terminated MSE queries now propagate errors to receivers instead of being dropped, avoiding timeout reports.

flowchart TD
  N0["QueryRunner calls register #40;F1#41;"]:::stModified
  N1["OpChainSchedulerService#46;register#58; lock and check termination #40;F2#41;"]:::stModified
  N2["Terminated#58; notify listener#44; cancel and close op chain #40;F2#41;"]:::stModified
  N3["Not terminated#58; proceed to registerInternal #40;F2#41;"]:::stModified
  N4["registerInternal#58; create ListenableFutureTask and wrap in OpChainTask #40;F2#41;"]:::stModified
  N5["registerInternal#58; execute via executorService#46;execute and register task with #40;F2#41;"]:::stModified
  N6["OpChainTask#46;run#58; set thread and run task #40;F2#41;"]:::stModified
  N7["ListenableFutureTask#46;runJob#58; check termination or run op chain #40;F2#41;"]:::stModified
  N8["FutureCallback#58; handle result and notify listener #40;F2#41;"]:::stModified
  N9["Catch exception from executor#46;execute#44; decrement count#44; rethrow #40;F2#41;"]:::stModified
  N10["QueryRunner catches exception and closes op chain #40;F1#41;"]:::stModified
  N0 -->|"call"| N1
  N1 -->|"if terminated"| N2
  N1 -->|"if not terminated"| N3
  N3 -->|"proceed"| N4
  N4 -->|"wrap and execute"| N5
  N5 -->|"task execution"| N6
  N5 -->|"if executor throws"| N9
  N6 -->|"run task"| N7
  N7 -->|"task completion"| N8
  N9 -->|"rethrow"| N10
  classDef stAdded fill:#dafbe1,stroke:#1a7f37,color:#1f2328,stroke-width:2px
  classDef stModified fill:#fff8c5,stroke:#9a6700,color:#1f2328,stroke-width:2px
  classDef stRemoved fill:#ffebe9,stroke:#cf222e,color:#1f2328,stroke-width:2px
  classDef stUnchanged fill:#f6f8fa,stroke:#656d76,color:#1f2328,stroke-width:1px
Loading

AI-generated · Green: added · Yellow: modified · Red: removed · Gray: existing

Diff evidence
  • F1: pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/QueryRunner.java — before · after
  • F2: pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/executor/OpChainSchedulerService.java — before · after
  • Regenerate PR flow

Problem

When a server terminates a multi-stage query, a stage on that server that has not started yet sends nothing to its receivers. For example, the resource accountant terminates a query to protect the heap. The broker then waits until the query timeout and reports a timeout instead of the kill.

Server terminates the query Master This PR
Before it schedules the stages 250 "Timed out on stage 0 waiting for data from child stage 1", after the full timeout 245 with the kill message, in less than 1 s
After it schedules the op chains, before they start 250, after the full timeout 245 with the kill message, in less than 1 s

Two paths drop the op chain:

  1. OpChainSchedulerService scheduled the op chain with ExecutorService.submit(). The context-aware executor registers the returned Future with the QueryExecutionContext, and terminate() cancels it. A FutureTask cancelled before it starts never runs its task, so the FutureCallback that cancels and closes the op chain never runs. The server logs nothing.
  2. register() rejected the op chain of a terminated query. QueryRunner then closed the op chain, but GrpcSendingMailbox.close() sends nothing when no stream is open yet. The error op chain from OpChainConverter.sendEarlyError also went through register(), and the same check rejected it ("Failed to propagate stage error via OpChainConverter").

On the same path, a pipeline breaker cancelled before it ran looked like it collected no rows. Also, sendEarlyError replaced the error code with QUERY_EXECUTION, so a forwarded kill lost its SERVER_RESOURCE_LIMIT_EXCEEDED code.

Change

  • register() no longer rejects the op chain of a terminated query, including a query cancelled through the cancelled-query cache. It cancels the op chain with the termination error on the calling thread, without the executor. This sends the error code and message to the receivers.
  • The scheduler submits a small OpChainTask with execute() and registers it with the query context. Cancelling it interrupts the op chain only while it runs. A queued op chain still runs, fails before it does any work, and the existing failure callback sends the termination error.
  • PipelineBreakerOperator.cancel() keeps the cancel reason as its error block.
  • OpChainConverter.sendEarlyError keeps the error code of the error block.
  • The Javadoc of QueryThreadContext.contextAwareExecutorService now says that only submit() registers tasks for cancellation.

There is no change to configuration, SPI or wire format.

Tests

  • QueryTerminatedBeforeStageStartsTest (new) runs two servers and a broker reducer. One server terminates the query:

    • before it schedules the stages, for a group-by query and for a dynamic-broadcast join (a pipeline breaker);
    • after it schedules the op chains, but before they start. GatedExecutorServicePlugin, a test executor, holds the op chains until the test terminates the query.

    The broker must report SERVER_RESOURCE_LIMIT_EXCEEDED with the kill message within 10 s. The query timeout is 30 s.

  • OpChainSchedulerServiceTest: an op chain that waits in the executor queue when the query is terminated, op chains registered after a termination and after a cancel, and no interrupt that leaks to the next task of the thread.

  • OpChainConverterDispatcherTest: sendEarlyError keeps the error code.

  • HardLimitExecutorOpChainRegressionTest (from Check HardLimitExecutor limit only once, on submit #19864) cast the queued task to Future, which assumed that the scheduler uses submit(). The test now runs the task directly and checks that the op chain ran and closed, so it holds with submit() and with execute().

All the new and changed tests fail on master. The pinot-query-runtime tests, QueryKillingIntegrationTest, WindowResourceAccountingTest and MultiStageEngineIntegrationTest pass.

Not in this PR: a task can still time out in the heap throttling queue and never run. (#19864 removed the run-time limit check of HardLimitExecutor.)

@yashmayya yashmayya added bug Something is not working as expected multi-stage Related to the multi-stage query engine labels Oct 13, 2026
@codecov-commenter

codecov-commenter commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.39%. Comparing base (c15cead) to head (34a71a3).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19854      +/-   ##
============================================
+ Coverage     69.26%   69.39%   +0.12%     
  Complexity     1491     1491              
============================================
  Files          3543     3544       +1     
  Lines        233237   233458     +221     
  Branches      37277    37316      +39     
============================================
+ Hits         161560   162007     +447     
+ Misses        59184    58942     -242     
- Partials      12493    12509      +16     
Flag Coverage Δ
integration 100.00% <ø> (ø)
integration1 100.00% <ø> (ø)
integration2 0.00% <ø> (ø)
java-25 69.39% <100.00%> (+0.12%) ⬆️
lane-a 100.00% <ø> (ø)
lane-b 0.00% <ø> (ø)
temurin 69.39% <100.00%> (+0.12%) ⬆️
unittests 69.39% <100.00%> (+0.12%) ⬆️
unittests1 58.86% <100.00%> (+0.02%) ⬆️
unittests2 41.21% <0.00%> (+0.27%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yashmayya
yashmayya requested a review from gortiz October 13, 2026 22:10

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff against the op-chain termination/lifecycle flow. This is a clean, well-reasoned fix with excellent docs and tests, and I couldn't find any blocking issues.

What I checked:

  • The register() terminated path now cancels + closes the op chain on the calling thread and fires the completion listener. It doesn't go through registerInternal, so it never touches _activeOpChainsByRequest/_executionContextByRequest — self-consistent, no counter leak, and the listener still fires exactly once per op chain.
  • execute() vs submit(): confirmed DecoratorExecutorService.execute() still decorates (sets up QueryThreadContext) but skips onSubmit/addTask, so the op chain is no longer auto-cancelled-and-dropped. The manual addTask(opChainTask) after execute() restores interrupt-based cancellation without the drop.
  • OpChainTask interrupt handling: the "null the thread → Thread.interrupted() → set(null)" ordering correctly closes the interrupt-leak window, and interruptTask() is a no-op once _thread is cleared. The dedicated test (shouldNotLeakInterruptAfterOpChainCompletes) exercises exactly this.
  • Rejected-execution path still decrements the active counter once and rethrows; no double-decrement with the FutureCallback.
  • sendEarlyError and PipelineBreakerOperator.cancel now preserve the real error code (TerminationException extends QueryException, so fromException keeps SERVER_RESOURCE_LIMIT_EXCEEDED).

A couple of minor/nit comments inline — none blocking.

Severity counts: CRITICAL: 0, MAJOR: 0, MINOR: 2.

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff against the op-chain termination/lifecycle flow. This is a clean, well-reasoned fix with excellent docs and tests, and I couldn't find any blocking issues.

What I checked:

  • The register() terminated path now cancels + closes the op chain on the calling thread and fires the completion listener. It doesn't go through registerInternal, so it never touches _activeOpChainsByRequest/_executionContextByRequest — self-consistent, no counter leak, and the listener still fires exactly once per op chain.
  • execute() vs submit(): confirmed DecoratorExecutorService.execute() still decorates (sets up QueryThreadContext) but skips onSubmit/addTask, so the op chain is no longer auto-cancelled-and-dropped. The manual addTask(opChainTask) after execute() restores interrupt-based cancellation without the drop.
  • OpChainTask interrupt handling: the "null the thread → Thread.interrupted() → set(null)" ordering correctly closes the interrupt-leak window, and interruptTask() is a no-op once _thread is cleared. The dedicated test (shouldNotLeakInterruptAfterOpChainCompletes) exercises exactly this.
  • Rejected-execution path still decrements the active counter once and rethrows; no double-decrement with the FutureCallback.
  • sendEarlyError and PipelineBreakerOperator.cancel now preserve the real error code (TerminationException extends QueryException, so fromException keeps SERVER_RESOURCE_LIMIT_EXCEEDED).

A couple of minor/nit comments inline — none blocking.

Severity counts: CRITICAL: 0, MAJOR: 0, MINOR: 2.

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed on the new head (7ef306d) after the push. The production logic is unchanged from my previous approval, and the new revision addresses both of my earlier nits:

  • PipelineBreakerOperator.cancel now documents why the cancel reason may be a non-Exception (Error/OutOfMemoryError), clarifying the instanceof Exception branch.
  • The OpChainTask.run interrupt-clear comment now explains the happens-before with interruptTask().

The new PipelineBreakerExecutorTest cases close the coverage gap Codecov flagged:

  • shouldReturnTerminationErrorWhenQueryIsTerminatedBeforePBStarts — PB cancelled before it runs keeps the termination error instead of looking like zero rows.
  • shouldReturnErrorBlockWhenPBFailsWithError — the OutOfMemoryError (non-Exception) cancel path is now exercised.

I verified the new tests reference real APIs (PipelineBreakerResult.getErrorBlock(), executePipelineBreakers, the _mailbox1/MAILBOX_ID_1 fixtures) and follow the existing test patterns. No CRITICAL/MAJOR issues; re-approving.

Severity counts: CRITICAL: 0, MAJOR: 0, MINOR: 0 (prior nits resolved).

@xiangfu0 xiangfu0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed on the new head (34a71a3) after the push. The production logic is unchanged from my prior approvals; the only substantive change is a necessary test alignment plus a rebase onto newer master:

  • HardLimitExecutorOpChainRegressionTest no longer asserts the queued task is a FutureTask and no longer unwraps it via Future.get(). Since the scheduler now hands op chains to the executor with execute() (not submit()), the queued task is the OpChainTask runnable directly. Running it on the test thread means an executor exception propagates straight to the test — a stronger check than the old swallowed-then-rethrown path. The dropped java.util.concurrent.Future import is consistent.
  • QueryThreadContext shows only a shifted hunk context header (rebase onto master that added mergeRemoteAccessStats upstream); the actual contextAwareExecutorService Javadoc change is identical to what I reviewed.

No CRITICAL/MAJOR issues; re-approving.

Severity counts: CRITICAL: 0, MAJOR: 0, MINOR: 0.

@yashmayya
yashmayya merged commit 1ecbd5c into apache:master Oct 16, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something is not working as expected multi-stage Related to the multi-stage query engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants