Skip to content

Commit 48dd732

Browse files
charles-typmeta-codesync[bot]
authored andcommitted
Fix server IO->CPU dispatch so rpc_num_cpu_worker_threads > 1 works (#749)
Summary: Pull Request resolved: #749 Setting `rpc_num_cpu_worker_threads > 1` made the server go completely idle (~2% CPU, requests never processed) during a benchmark, so the flag was unusable and the prod-like "IO -> CPU -> IO" dispatch pattern it is meant to exercise never actually ran. Root cause: the Thrift handlers are `async_eb_*`, so they must complete their `HandlerCallback` on the callback's own EventBase. `ucacheBenchOnRequestCommon` offloaded work to a `folly::CPUThreadPoolExecutor` (from `getCpuPool()`, only created when the flag is > 1) and then invoked the handler — which called `cb->result(...)` — directly on the CPU-pool thread. Those pool threads are not EventBase threads and are unknown to the per-EventBase handler map, so the reply was never delivered. In-flight requests never drained, clients stopped sending, and the server sat idle. With the flag == 1, `getCpuPool()` returns `nullptr`, everything runs inline on the IO thread, and it works — which is why the bug only showed up for > 1. Fix: split compute from delivery. The per-request `handler` now COMPUTES and returns the reply; `ucacheBenchOnRequestCommon` owns delivering it via `callback->result(...)`. On the CPU-pool path it captures the callback's EventBase up front, runs the compute on the pool, then bounces the completion back with `evb->runInEventBaseThread(...)` so the async_eb callback is completed on its own EventBase. The inline and fiber paths are behaviorally unchanged for the default `rpc_num_cpu_worker_threads == 1`. Verified: with the fix, `rpc_num_cpu_worker_threads=64` drives real traffic and the run completes cleanly ("All clients finished benchmark"), where before it hung the server at ~2% CPU. Note: the dispatch path trades throughput for the extra IO->CPU->IO context switches (as intended, to match production's tao:slow scheduling); it is a fidelity knob, not a throughput/utilization lever. Reviewed By: excelle08 Differential Revision: D110506501 fbshipit-source-id: afd5931e5f78d85b7ead8bf87c1fda97a5bca37f
1 parent 1b62548 commit 48dd732

2 files changed

Lines changed: 33 additions & 17 deletions

File tree

packages/ucache_bench/server/UcacheBenchOnRequest.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,26 @@ void UcacheBenchOnRequest::onRequestThrift(
2121
apache::thrift::HandlerCallbackPtr<UcbGetReply> callback,
2222
UcbGetRequest&& request) {
2323
ucacheBenchOnRequestCommon(
24-
std::move(callback), std::move(request), [this](auto&& cb, auto&& req) {
25-
cb->result(server_->processUcbGetSync(req));
24+
std::move(callback), std::move(request), [this](auto&& req) {
25+
return server_->processUcbGetSync(req);
2626
});
2727
}
2828

2929
void UcacheBenchOnRequest::onRequestThrift(
3030
apache::thrift::HandlerCallbackPtr<UcbSetReply> callback,
3131
UcbSetRequest&& request) {
3232
ucacheBenchOnRequestCommon(
33-
std::move(callback), std::move(request), [this](auto&& cb, auto&& req) {
34-
cb->result(server_->processUcbSetSync(req));
33+
std::move(callback), std::move(request), [this](auto&& req) {
34+
return server_->processUcbSetSync(req);
3535
});
3636
}
3737

3838
void UcacheBenchOnRequest::onRequestThrift(
3939
apache::thrift::HandlerCallbackPtr<UcbDeleteReply> callback,
4040
UcbDeleteRequest&& request) {
4141
ucacheBenchOnRequestCommon(
42-
std::move(callback), std::move(request), [this](auto&& cb, auto&& req) {
43-
cb->result(server_->processUcbDeleteSync(req));
42+
std::move(callback), std::move(request), [this](auto&& req) {
43+
return server_->processUcbDeleteSync(req);
4444
});
4545
}
4646

@@ -49,12 +49,12 @@ void UcacheBenchOnRequest::onRequestThrift(
4949
callback,
5050
facebook::memcache::McVersionRequest&& request) {
5151
ucacheBenchOnRequestCommon(
52-
std::move(callback), std::move(request), [](auto&& cb, auto&& /* req */) {
52+
std::move(callback), std::move(request), [](auto&& /* req */) {
5353
facebook::memcache::McVersionReply reply;
5454
reply.result() = carbon::Result::FOUND;
5555
reply.value() =
5656
*folly::IOBuf::copyBuffer("UcacheBench 1.0 (with Fiber support)");
57-
cb->result(std::move(reply));
57+
return reply;
5858
});
5959
}
6060

packages/ucache_bench/server/UcacheBenchRequestCommon.h

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <fmt/format.h>
66
#include <folly/executors/CPUThreadPoolExecutor.h>
7+
#include <folly/io/async/EventBase.h>
78
#include <folly/portability/GFlags.h>
89
#include <mcrouter/lib/carbon/Result.h>
910
#include "UcacheBenchIOThreadContext.h"
@@ -30,7 +31,16 @@ inline folly::CPUThreadPoolExecutor* getCpuPool() {
3031
}
3132

3233
/**
33-
* Common entry point to run request with fiber management
34+
* Common entry point to run a request.
35+
*
36+
* `handler` COMPUTES and returns the reply (it must not complete the callback
37+
* itself). This function owns delivering the reply via `callback->result(...)`.
38+
* That split matters for the IO→CPU→IO path: these are async_eb handlers, so
39+
* the callback MUST be completed on its own EventBase. When work is offloaded
40+
* to the CPU pool we compute the reply there, then bounce the completion back
41+
* to the callback's EventBase via runInEventBaseThread(). Completing an
42+
* async_eb callback directly from a CPU-pool thread never delivers the reply —
43+
* in-flight requests never drain and the server goes idle.
3444
*/
3545
template <class Callback, class Request, class Handler>
3646
void ucacheBenchOnRequestCommon(
@@ -41,27 +51,33 @@ void ucacheBenchOnRequestCommon(
4151
{
4252
auto* cpuPool = getCpuPool();
4353
if (cpuPool) {
54+
auto* evb = callback->getEventBase();
4455
cpuPool->add([handler,
4556
cb = std::forward<Callback>(callback),
46-
req = std::forward<Request>(request)]() mutable {
47-
handler(std::move(cb), std::move(req));
57+
req = std::forward<Request>(request),
58+
evb]() mutable {
59+
auto reply = handler(req);
60+
evb->runInEventBaseThread(
61+
[cb = std::move(cb), reply = std::move(reply)]() mutable {
62+
cb->result(std::move(reply));
63+
});
4864
});
4965
return;
5066
}
5167

52-
// If fibers are disabled, execute directly
68+
// If fibers are disabled, execute directly on the IO thread.
5369
if (!FLAGS_enable_fibers ||
5470
!UcacheBenchIOThreadContext::isInitializedForCurrentThread()) {
55-
handler(std::forward<Callback>(callback), std::forward<Request>(request));
71+
callback->result(handler(request));
5672
return;
5773
}
5874

59-
// Execute the handler in a fiber
75+
// Execute the handler in a fiber (so simulateIOLatency can yield).
6076
UcacheBenchIOThreadContext::tlInstance().fm().addTaskEager(
6177
[handler,
62-
callbackFiber = std::forward<Callback>(callback),
63-
requestFiber = std::forward<Request>(request)]() mutable {
64-
handler(std::move(callbackFiber), std::move(requestFiber));
78+
cb = std::forward<Callback>(callback),
79+
req = std::forward<Request>(request)]() mutable {
80+
cb->result(handler(req));
6581
});
6682
}
6783

0 commit comments

Comments
 (0)