Skip to content

fix(native): Drain httpSrvCpuExecutor before driverExecutor on shutdown - #28491

Open
pdabre12 wants to merge 1 commit into
prestodb:masterfrom
pdabre12:offload-sidecar-exprs-to-CPU
Open

pdabre12 wants to merge 1 commit into
prestodb:masterfrom
pdabre12:offload-sidecar-exprs-to-CPU

Conversation

@pdabre12

@pdabre12 pdabre12 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Description

Ensure native server shutdown drains HTTP server CPU executor before joining the driver executor.

Motivation and Context

The /v1/expressions endpoint dispatches its work to httpSrvCpuExecutor_, which passes a raw driverExecutor_.get() pointer into QueryCtx. Before this fix, joinExecutors() called driverExecutor_.reset() while those threads could still be running — a use-after-free. httpSrvCpuExecutor_->join() was originally placed near the end of joinExecutors(), well after driverExecutor_ was already destroyed.

Impact

Better performance and removes silent memory corruption.

Test Plan

Unit tests, CI

Contributor checklist

  • Please make sure your submission complies with our contributing guide, in particular code style and commit standards.
  • PR description addresses the issue accurately and concisely. If the change is non-trivial, a GitHub Issue is referenced.
  • Documented new properties (with its default value), SQL syntax, functions, or other functionality.
  • If release notes are required, they follow the release notes guidelines.
  • Adequate tests were added if applicable.
  • CI passed.
  • If adding new dependencies, verified they have an OpenSSF Scorecard score of 5.0 or higher (or obtained explicit TSC approval for lower scores).

Release Notes

Please follow release notes guidelines and fill in the release notes below.

== NO RELEASE NOTE ==

Summary by Sourcery

Safely drain HTTP server CPU work before joining the driver executor during native server shutdown.

Bug Fixes:

  • Prevent native server shutdown from destroying the driver executor before HTTP expression-processing tasks that still reference it have completed.

Enhancements:

  • Align executor shutdown ordering so HTTP server CPU and exchange work are drained before the driver executor is joined.
  • Reduce unnecessary JSON result copying in sidecar endpoint response handling.

Tests:

  • Add shutdown-order coverage for HTTP server tasks dispatching work through the driver executor.

@prestodb-ci prestodb-ci added the from:IBM PR from IBM label Sep 15, 2026
@sourcery-ai

sourcery-ai Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Fixes native server shutdown ordering by draining the HTTP server CPU executor before destroying the driver executor, adds a regression test for the use-after-free scenario, and optimizes sidecar result serialization by moving the result into JSON conversion.

Sequence diagram for safe native server shutdown ordering

sequenceDiagram
    participant Server as PrestoServer
    participant HttpCpu as httpSrvCpuExecutor_
    participant Query as HTTP task /v1/expressions
    participant Driver as driverExecutor_

    Server->>HttpCpu: join()
    HttpCpu->>Query: Finish queued CPU work
    Query->>Driver: Use driverExecutor_.get() via QueryCtx
    Driver-->>Query: Complete dispatched work
    HttpCpu-->>Server: HTTP CPU executor drained
    Server->>Driver: reset() / stop driver executor
    Note over Server,Driver: Driver executor is destroyed only after HTTP tasks finish
Loading

Sequence diagram for sidecar result serialization

sequenceDiagram
    participant Sidecar as Sidecar endpoint
    participant Cpu as httpSrvCpuExecutor_
    participant Json as JSON serializer

    Sidecar->>Cpu: thenValue(result)
    Cpu->>Json: json(std::move(result))
    Json-->>Cpu: Serialized JSON bytes
    Cpu-->>Sidecar: dumpJson result
Loading

File-Level Changes

Change Details Files
Reorders native shutdown so HTTP server CPU tasks are fully drained before the driver executor is destroyed, preventing raw-pointer use-after-free during expression handling.
  • Moves the HTTP server CPU executor join to the beginning of executor shutdown.
  • Retains exchange HTTP CPU draining before driver executor teardown and documents both dependency orderings.
  • Adds shutdown logging for the earlier HTTP server CPU join.
presto-native-execution/presto_cpp/main/PrestoServer.cpp
Adds a regression test that models HTTP server work dispatching to the driver executor during shutdown.
  • Synchronizes an HTTP CPU task before it submits work through a raw driver executor pointer.
  • Verifies joining the HTTP executor before resetting the driver executor completes safely.
presto-native-execution/presto_cpp/main/tests/ShutdownOrderTest.cpp
Avoids copying sidecar endpoint results during JSON serialization.
  • Moves the result into the JSON conversion before dumping it.
presto-native-execution/presto_cpp/main/PrestoServer.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@pdabre12
pdabre12 force-pushed the offload-sidecar-exprs-to-CPU branch from 286840e to ee3a0a3 Compare September 15, 2026 22:58
@pdabre12
pdabre12 marked this pull request as ready for review September 15, 2026 23:02
@pdabre12
pdabre12 requested review from a team, pgupta2 and singcha as code owners September 15, 2026 23:02
@prestodb-ci
prestodb-ci requested review from a team and devlin-lee and removed request for a team September 15, 2026 23:02

@sourcery-ai sourcery-ai Bot 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@aditi-pandit
aditi-pandit requested a balanced review from Copilot September 16, 2026 00:18

This comment was marked as outdated.

Copilot AI 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.

🟡 Changes recommended

The regression test hard-codes the expected order and would pass if the production fix were reverted.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +206 to +207
// Correct order: drain httpSrvCpu before destroying driverExecutor.
httpSrvCpu->join();

@pdabre12 pdabre12 Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thats how the above test cases do it as well in ShutdownOrderTest so I think its okay to test it like this for now.

@aditi-pandit

Copy link
Copy Markdown
Contributor

@amitkdutta : Hi Amit, Can you take a look at this PR as well since its in the main PrestoServer.cpp path ?

@aditi-pandit aditi-pandit 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 for shutdown/lifetime safety. The use-after-free this fixes is real, but reordering joinExecutors() does not fully resolve it — httpSrvCpuExecutor_ has a dependency in the opposite direction too, so the new order trades one hazard for another. Details inline; folly behaviour below is from the vendored copy under presto-native-execution/folly.

Summary of findings:

  1. httpSrvCpuExecutor_ is TaskManager's continuation executor, so joining it before the driver pool inverts a second, opposing constraint (inline on PrestoServer.cpp).
  2. The keepalive wait inside join() now runs first, moving the shutdown-hang risk to the front of joinExecutors() with nothing else drained (inline on PrestoServer.cpp).
  3. The regression test does not pin the production ordering (inline on ShutdownOrderTest.cpp) — agreeing with Copilot's comment here.
  4. json(std::move(result)) is safe but not applied to the sibling handler (inline on PrestoServer.cpp).

Nothing here blocks on its own; finding 1 is the one worth resolving before merge, because ordering alone cannot satisfy both directions of the dependency.

<< "': threads: " << httpSrvCpuExecutor_->numActiveThreads() << "/"
<< httpSrvCpuExecutor_->numThreads()
<< ", task queue: " << httpSrvCpuExecutor_->getTaskQueueSize();
httpSrvCpuExecutor_->join();

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.

reordering creates the mirror-image hazard

httpSrvCpuExecutor_ is not only the sidecar endpoints' executor. It is also TaskManager's and TaskResource's continuation executor: TaskManager.cpp lines 1104, 1122, 1143, 1157, 1179, 1186, 1213, 1228, 1251, 1294, 1314, 1327 all do .via(httpSrvCpuExecutor_). In particular prestoTask->task->stateChangeFuture(maxWaitMicros).via(httpSrvCpuExecutor_) (TaskManager.cpp:1143) is fulfilled by a driver thread on task state change.

So the dependency runs both ways:

  • sidecar tasks on httpSrvCpu → driverExecutor_ (the direction this PR fixes), and
  • driver threads → continuations scheduled onto httpSrvCpu (the direction this PR breaks).

Joining httpSrvCpu here, before driverCpuExecutor_->join() below, means a driver thread can touch an already-joined pool. What folly does in that case:

  • stopAndJoinAllThreads sets maxThreads_ = 0 and activeThreads_ = 0 (folly/executors/ThreadPoolExecutor.cpp:283-284). A later add() enqueues fine, then ensureActiveThreads() sees active >= total and starts no thread (ThreadPoolExecutor.cpp:531-536) — the task is silently never run.
  • .via(Executor*) acquires a keepalive, and DefaultKeepAliveExecutor::keepAliveAcquire asserts DCHECK(keepAliveCount > 0) (folly/DefaultKeepAliveExecutor.h:141-145). The count is 0 after join, so this fails in debug builds; in release it increments from 0 and the matching release re-posts keepAliveReleaseBaton_.

The only thing making this survivable today is that shutdownServer() destroys httpServer_ and taskManager_ before calling joinExecutors() (lines 947-948, 956). That is not a guarantee: TaskManager::shutdown() waits only on kRunning tasks, and the teardown path explicitly tolerates Velox tasks with outstanding references, logging "Velox task has pending reference on destruction" and continuing (TaskManager.cpp:1540-1555). Driver-side work can therefore still be live when httpSrvCpu is already joined.

Ordering alone cannot satisfy both directions. Suggest removing the raw-pointer dependency instead: hand QueryCtx::create an executor held by a keepalive, or run sidecar expression evaluation on an executor that is not the driver pool. getOptimizedExpressions (lines 224-267) is fully synchronous and only needs an executor for QueryCtx, so it does not need the driver pool specifically.

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.

@pdabre12 : I'm generally in favor of the suggestion
"Run sidecar expression evaluation on an executor that is not the driver pool. getOptimizedExpressions (lines 224-267) is fully synchronous and only needs an executor for QueryCtx, so it does not need the driver pool specifically."

This change might be changing current problems to something else. If you can implement this suggestion then that works best.

<< "': threads: " << httpSrvCpuExecutor_->numActiveThreads() << "/"
<< httpSrvCpuExecutor_->numThreads()
<< ", task queue: " << httpSrvCpuExecutor_->getTaskQueueSize();
httpSrvCpuExecutor_->join();

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.

shutdown-hang risk moves to the front of joinExecutors()

ThreadPoolExecutor::join() calls joinKeepAliveOnce()joinKeepAlive(), which blocks on keepAliveReleaseBaton_.wait() (folly/DefaultKeepAliveExecutor.h:51-55) until every KeepAlive token on this executor is released. Pending long-poll chains (getTaskInfo/getResults with maxWait) hold exactly those tokens.

Previously this wait ran last, after the driver, connector and exchange CPU pools were already drained. It is now the first thing joinExecutors() does, with every other pool still up and no timeout on the wait. Any token whose release depends on a stage that is joined later turns shutdown into a hang rather than a crash. Worth a comment recording the counter-constraint, so the next person does not reorder this blind — and note the existing exchange-before-driver ordering has no such note either.

sync->proceedEvent.notifyAll();

// Correct order: drain httpSrvCpu before destroying driverExecutor.
httpSrvCpu->join();

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.

test does not pin the production ordering

Agreeing with Copilot's comment: this hardcodes httpSrvCpu->join(); driverExecutor.reset(); in the test body, so reverting the PrestoServer.cpp change leaves the test green.

On the "the other tests in this file do it the same way" reply — that is accurate, but weak justification here. Pinning the shutdown order is this file's only purpose, and with two opposing constraints now in play (see the comment on PrestoServer.cpp) nothing stops the next reorder from reintroducing the bug in either direction. Extracting the ordering into a helper that joinExecutors() calls, and driving that helper from the test, would cost little and would actually protect the fix.

// Serialize on the CPU executor so the I/O thread only
// transmits pre-built bytes.
return util::dumpJson(json(result));
return util::dumpJson(json(std::move(result)));

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.

safe, but not applied to the sibling handler

The move is safe: result is the future's json::array_t&& and is not read afterwards.

The /v1/velox/plan handler below still does json(response) without the move (around line 2049), and response is equally dead after that point. Either apply the same change there or drop this hunk, so the two sidecar handlers stay consistent.

Minor: the PR description's "Better performance" claim rests entirely on this one line, not on the shutdown reordering.

@amitkdutta

Copy link
Copy Markdown
Contributor

I re-checked the current head (ee3a0a3) against the exact pinned Velox revision (9084390).

I do not think the new test demonstrates the claimed /v1/expressions use-after-free. The production path executes getOptimizedExpressions synchronously on httpSrvCpuExecutor_; it only stores driverExecutor_.get() in a temporary QueryCtx. At the pinned Velox revision, QueryCtx explicitly documents that no executor is required for expression evaluation, its constructor only stores the pointer, and the expression optimizer does not dispatch work to it.

By contrast, the test invents driverRawPtr->add(...), then hard-codes httpSrvCpu->join(); driverExecutor.reset();. It never calls PrestoServer::joinExecutors(), so reverting the production hunk still leaves the test green.

The narrow fix seems to be passing nullptr (or a dedicated non-driver executor) into QueryCtx for this synchronous endpoint, removing the lifetime edge entirely. If executor reordering is still needed for another real call path, please add a test that exercises that production path and fails when the production order is reverted.

I also could not substantiate a new driver-to-HTTP late-enqueue regression from the TaskManager .via(httpSrvCpuExecutor_) paths: those acquire executor keepalives, and join() waits for those keepalives before stopping workers. My blocking concern is therefore the synthetic test and unnecessary raw dependency, rather than a demonstrated mirror-image dependency.

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

Labels

from:IBM PR from IBM

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants