-
Notifications
You must be signed in to change notification settings - Fork 5.5k
fix(native): Drain httpSrvCpuExecutor before driverExecutor on shutdown #28491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -830,11 +830,22 @@ void PrestoServer::stopAnnouncer() { | |
| } | ||
|
|
||
| void PrestoServer::joinExecutors() { | ||
| // Join exchange HTTP CPU executor first. Exchange CPU threads run | ||
| // PrestoExchangeSource::handleDataResponse which dispatches callbacks to | ||
| // driverExecutor_ (MonitoredExecutor) via InMemoryExchangeClient. We must | ||
| // drain these threads before destroying driverExecutor_ to avoid | ||
| // use-after-free. | ||
| // Drain httpSrvCpuExecutor_ first: /v1/expressions tasks run here and pass | ||
| // driverExecutor_.get() into QueryCtx; they must finish before the driver | ||
| // pool is stopped. | ||
| if (httpSrvCpuExecutor_ != nullptr) { | ||
| PRESTO_SHUTDOWN_LOG(INFO) | ||
| << "Joining HTTP Server CPU Executor '" | ||
| << httpSrvCpuExecutor_->getName() | ||
| << "': threads: " << httpSrvCpuExecutor_->numActiveThreads() << "/" | ||
| << httpSrvCpuExecutor_->numThreads() | ||
| << ", task queue: " << httpSrvCpuExecutor_->getTaskQueueSize(); | ||
| httpSrvCpuExecutor_->join(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shutdown-hang risk moves to the front of joinExecutors()
Previously this wait ran last, after the driver, connector and exchange CPU pools were already drained. It is now the first thing |
||
| } | ||
|
|
||
| // Join exchange HTTP CPU executor before the driver executor. Exchange CPU | ||
| // threads dispatch callbacks to driverExecutor_ via InMemoryExchangeClient; | ||
| // drain them before destroying driverExecutor_ to avoid use-after-free. | ||
| PRESTO_SHUTDOWN_LOG(INFO) | ||
| << "Joining Exchange Http CPU executor '" | ||
| << exchangeHttpCpuExecutor_->getName() | ||
|
|
@@ -873,15 +884,6 @@ void PrestoServer::joinExecutors() { | |
| connectorIoExecutor_->join(); | ||
| } | ||
|
|
||
| if (httpSrvCpuExecutor_ != nullptr) { | ||
| PRESTO_SHUTDOWN_LOG(INFO) | ||
| << "Joining HTTP Server CPU Executor '" | ||
| << httpSrvCpuExecutor_->getName() | ||
| << "': threads: " << httpSrvCpuExecutor_->numActiveThreads() << "/" | ||
| << httpSrvCpuExecutor_->numThreads() | ||
| << ", task queue: " << httpSrvCpuExecutor_->getTaskQueueSize(); | ||
| httpSrvCpuExecutor_->join(); | ||
| } | ||
| if (httpSrvIoExecutor_ != nullptr) { | ||
| PRESTO_SHUTDOWN_LOG(INFO) | ||
| << "Joining HTTP Server IO Executor '" << httpSrvIoExecutor_->getName() | ||
|
|
@@ -1994,7 +1996,7 @@ void PrestoServer::registerSidecarEndpoints() { | |
| .thenValue([](auto&& result) { | ||
| // 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))); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. safe, but not applied to the sibling handler The move is safe: The Minor: the PR description's "Better performance" claim rests entirely on this one line, not on the shutdown reordering. |
||
| }) | ||
| .via( | ||
| folly::getKeepAliveToken( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -173,4 +173,41 @@ TEST(ShutdownOrderTest, promiseChainDispatchesSafelyDuringShutdown) { | |
| // call would dispatch to a freed executor. | ||
| } | ||
|
|
||
| // /v1/expressions tasks run on httpSrvCpuExecutor_ and pass | ||
| // driverExecutor_.get() into QueryCtx. httpSrvCpuExecutor_ must be drained | ||
| // before driverExecutor_ is reset; httpSrvCpu->join() serialises the task's | ||
| // driverRawPtr->add() against driverExecutor.reset() — swapping those two calls | ||
| // would be use-after-free. | ||
| TEST(ShutdownOrderTest, httpSrvTasksDrainBeforeDriverExecutorJoined) { | ||
| auto driverExecutor = std::make_unique<NoKeepAliveExecutor>( | ||
| std::make_unique<folly::CPUThreadPoolExecutor>( | ||
| 2, std::make_shared<folly::NamedThreadFactory>("TestDriver"))); | ||
| auto httpSrvCpu = std::make_unique<folly::CPUThreadPoolExecutor>( | ||
| 2, std::make_shared<folly::NamedThreadFactory>("TestHttpSrvCPU")); | ||
|
|
||
| auto* driverRawPtr = driverExecutor.get(); | ||
| auto sync = std::make_shared<SyncState>(); | ||
|
|
||
| // Simulate a /v1/expressions task: runs on httpSrvCpu, dispatches to | ||
| // driverExecutor via raw pointer (use-after-free if driverExecutor is gone). | ||
| httpSrvCpu->add([sync, driverRawPtr]() { | ||
| sync->exchangeReadyFlag = true; | ||
| sync->exchangeReadyEvent.notifyAll(); | ||
| sync->proceedEvent.await([sync]() { return sync->proceedFlag.load(); }); | ||
| driverRawPtr->add([]() {}); | ||
| sync->completed = true; | ||
| }); | ||
|
|
||
| sync->exchangeReadyEvent.await( | ||
| [sync]() { return sync->exchangeReadyFlag.load(); }); | ||
| sync->proceedFlag = true; | ||
| sync->proceedEvent.notifyAll(); | ||
|
|
||
| // Correct order: drain httpSrvCpu before destroying driverExecutor. | ||
| httpSrvCpu->join(); | ||
|
Comment on lines
+206
to
+207
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thats how the above test cases do it as well in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
| driverExecutor.reset(); | ||
| httpSrvCpu.reset(); | ||
|
|
||
| EXPECT_TRUE(sync->completed); | ||
| } | ||
| } // namespace facebook::presto | ||
There was a problem hiding this comment.
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.cpplines 1104, 1122, 1143, 1157, 1179, 1186, 1213, 1228, 1251, 1294, 1314, 1327 all do.via(httpSrvCpuExecutor_). In particularprestoTask->task->stateChangeFuture(maxWaitMicros).via(httpSrvCpuExecutor_)(TaskManager.cpp:1143) is fulfilled by a driver thread on task state change.So the dependency runs both ways:
driverExecutor_(the direction this PR fixes), andJoining httpSrvCpu here, before
driverCpuExecutor_->join()below, means a driver thread can touch an already-joined pool. What folly does in that case:stopAndJoinAllThreadssetsmaxThreads_ = 0andactiveThreads_ = 0(folly/executors/ThreadPoolExecutor.cpp:283-284). A lateradd()enqueues fine, thenensureActiveThreads()seesactive >= totaland starts no thread (ThreadPoolExecutor.cpp:531-536) — the task is silently never run..via(Executor*)acquires a keepalive, andDefaultKeepAliveExecutor::keepAliveAcquireassertsDCHECK(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-postskeepAliveReleaseBaton_.The only thing making this survivable today is that
shutdownServer()destroyshttpServer_andtaskManager_before callingjoinExecutors()(lines 947-948, 956). That is not a guarantee:TaskManager::shutdown()waits only onkRunningtasks, 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::createan 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 forQueryCtx, so it does not need the driver pool specifically.There was a problem hiding this comment.
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.