Skip to content

Commit 9ded98d

Browse files
charles-typfacebook-github-bot
authored andcommitted
Fix completion-hang deadlock so all clients report BENCHMARK_DONE (facebookresearch#699)
Summary: The distributed run never finalized cleanly: warmup completed (`warmup_done=N`) but `benchmark_done` stalled below `num_clients` (e.g. 13/16). Because the admin server only broadcasts `ALL_DONE` once `benchmarkCompleteClients_ >= numExpectedClients_`, the missing clients block EVERY client forever at `waitForNotification(0)`, so the run is torn down with no clean client-side results (only server-aggregated metrics survive). Root cause is a `google::log_mutex`/stdio deadlock, not a coroutine hang. `gstack` on a stuck client showed 48 of 189 threads blocked forever (`futex abstime=0x0`) on `google::log_mutex` inside `LogMessage::Flush()`; the lock holder was blocked in `__libc_write(fd=1)` to stdout, its buffer full of `Benchmark GET error (sample 1/1000): mc_res_tko` repeated. The stdout/stderr pipe to benchpress/automark was backpressured, so `write()` blocked while holding the stdio FILE lock and glog's `log_mutex`, wedging every logging thread including the coro worker event-base threads -> `mainScope.joinAsync()` never returns -> `BENCHMARK_DONE` is never sent. Under the `mc_res_tko` storm at multi-M QPS, even 1-in-1000 error sampling is thousands of synchronous prints/sec, which is enough to back the pipe up. Fix: the four hot-path sampled error prints now fire only on the FIRST error per worker (`errors == 1` instead of `errors % 1000 == 1`) -> at most one line per worker for the whole run, flood-proof, while keeping the error-type diagnostic (still gated by `--verbose`). The error counts themselves are unchanged and still reported at the end. This change also folds in the related straggler-drain robustness the completion path needs: per-request `co_await` now uses `folly::makePromiseContract` + `SemiFuture::within(10s)` (global-timekeeper based, abandons the underlying future so the await always completes even if the McRouter callback never fires), and the warmup/measurement worker scopes use `CancellableAsyncScope::cancelAndJoinAsync()` to cancel rather than wait on requests still outstanding at window-end. This entire diff stack contributed to the final result. Differential Revision: D110091576
1 parent 09207c2 commit 9ded98d

1 file changed

Lines changed: 43 additions & 31 deletions

File tree

packages/ucache_bench/client/UcacheBenchClient.cpp

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
#include <folly/coro/Sleep.h>
2929
#include <folly/coro/Timeout.h>
3030
#include <folly/fibers/FiberManagerMap.h>
31+
#include <folly/futures/Future.h>
32+
#include <folly/futures/Promise.h>
3133
#include <folly/io/async/ScopedEventBaseThread.h>
3234
#include <folly/portability/GFlags.h>
3335
#include <mcrouter/McrouterFiberContext.h>
@@ -991,8 +993,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
991993
request.value_ref() = *folly::IOBuf::copyBuffer(value);
992994
request.exptime_ref() = 3600;
993995

994-
auto [promise, future] =
995-
folly::coro::makePromiseContract<UcbSetReply>();
996+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
996997

997998
clientPtr->send(
998999
request,
@@ -1002,8 +1003,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
10021003
});
10031004

10041005
try {
1005-
co_await folly::coro::timeout(
1006-
std::move(future), std::chrono::seconds(10));
1006+
co_await std::move(future).within(std::chrono::seconds(10));
10071007
scanOps++;
10081008
} catch (const std::exception&) {
10091009
// Request timed out or failed (e.g. send() returned false and the
@@ -1107,7 +1107,9 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11071107
auto warmupWorker =
11081108
[&](memcache::mcrouter::CarbonRouterClient<UcacheBenchRouterInfo>*
11091109
clientPtr) -> folly::coro::Task<void> {
1110-
folly::coro::AsyncScope scope;
1110+
// CancellableAsyncScope so stragglers at warmup-end are cancelled, not
1111+
// waited on forever (same hang as the measurement worker — see below).
1112+
folly::coro::CancellableAsyncScope scope;
11111113
auto exe = co_await folly::coro::co_current_executor;
11121114

11131115
std::atomic<uint64_t> inflight{0};
@@ -1141,7 +1143,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11411143
}
11421144

11431145
// Same pattern as production McrouterAdapter::coro()
1144-
auto [promise, future] = folly::coro::makePromiseContract<UcbSetReply>();
1146+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
11451147

11461148
clientPtr->send(
11471149
request,
@@ -1152,8 +1154,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11521154

11531155
UcbSetReply result;
11541156
try {
1155-
result = co_await folly::coro::timeout(
1156-
std::move(future), std::chrono::seconds(10));
1157+
result = co_await std::move(future).within(std::chrono::seconds(10));
11571158
} catch (const std::exception&) {
11581159
// Request never completed (send() rejected without callback, or stuck
11591160
// pre-send during the connection storm). Count as error and return so
@@ -1169,11 +1170,15 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11691170
localSuccesses++;
11701171
} else {
11711172
localErrors++;
1172-
// Rate-limit verbose error output to avoid flooding stdout pipe
1173-
// which can deadlock when run through benchpress/automark
1174-
if (FLAGS_verbose && (localErrors.load() % 1000 == 1)) {
1173+
// Print only the FIRST error per worker. Any periodic sampling (even
1174+
// 1/1000) floods stdout under a TKO storm at multi-M QPS; the
1175+
// synchronous write() blocks on the backpressured benchpress/automark
1176+
// pipe while holding the stdio FILE lock and glog's log_mutex, wedging
1177+
// every worker thread -> joinAsync never returns -> BENCHMARK_DONE is
1178+
// never sent.
1179+
if (FLAGS_verbose && localErrors.load() == 1) {
11751180
printf(
1176-
"Warmup SET error (sample 1/1000): %s\n",
1181+
"Warmup SET error (first sample): %s\n",
11771182
carbon::resultToString(*result.result_ref()));
11781183
}
11791184
}
@@ -1224,7 +1229,8 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
12241229
co_await folly::futures::sleep(std::chrono::milliseconds(1));
12251230
}
12261231

1227-
co_await scope.joinAsync();
1232+
// Warmup window over: cancel stragglers and join (deterministic drain).
1233+
co_await scope.cancelAndJoinAsync();
12281234

12291235
// Final update - add any remaining counts not yet synced
12301236
uint64_t finalSuccesses = localSuccesses.load();
@@ -1628,7 +1634,14 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16281634
[&](size_t workerId,
16291635
memcache::mcrouter::CarbonRouterClient<UcacheBenchRouterInfo>*
16301636
clientPtr) -> folly::coro::Task<void> {
1631-
folly::coro::AsyncScope scope;
1637+
// CancellableAsyncScope (not AsyncScope) so that when the measurement
1638+
// window ends we CANCEL any still-outstanding requests instead of waiting
1639+
// for them indefinitely. A small number of in-flight requests at endTime
1640+
// can have responses that never arrive (and the per-request timeout's timer
1641+
// may sit on a momentarily-saturated proxy event base), which otherwise
1642+
// hangs joinAsync forever -> the client never sends BENCHMARK_DONE -> the
1643+
// whole run stalls. Cancelling at window-end is the correct semantics.
1644+
folly::coro::CancellableAsyncScope scope;
16321645
auto exe = co_await folly::coro::co_current_executor;
16331646

16341647
// Send one GET request - matches production McrouterAdapter::coro() pattern
@@ -1651,7 +1664,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16511664
}
16521665

16531666
// Same pattern as production McrouterAdapter::coro()
1654-
auto [promise, future] = folly::coro::makePromiseContract<UcbGetReply>();
1667+
auto [promise, future] = folly::makePromiseContract<UcbGetReply>();
16551668

16561669
clientPtr->send(
16571670
request,
@@ -1662,8 +1675,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16621675

16631676
UcbGetReply result;
16641677
try {
1665-
result = co_await folly::coro::timeout(
1666-
std::move(future), std::chrono::seconds(10));
1678+
result = co_await std::move(future).within(std::chrono::seconds(10));
16671679
} catch (const std::exception&) {
16681680
// Request never completed; count as a GET error and return so the
16691681
// measurement worker's scope.joinAsync() can't hang.
@@ -1712,7 +1724,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17121724
}
17131725

17141726
auto [setPromise, setFuture] =
1715-
folly::coro::makePromiseContract<UcbSetReply>();
1727+
folly::makePromiseContract<UcbSetReply>();
17161728

17171729
clientPtr->send(
17181730
setRequest,
@@ -1723,8 +1735,8 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17231735

17241736
UcbSetReply setResult;
17251737
try {
1726-
setResult = co_await folly::coro::timeout(
1727-
std::move(setFuture), std::chrono::seconds(10));
1738+
setResult =
1739+
co_await std::move(setFuture).within(std::chrono::seconds(10));
17281740
} catch (const std::exception&) {
17291741
workerSetOps[workerId]->fetch_add(1);
17301742
workerSetErrors[workerId]->fetch_add(1);
@@ -1736,18 +1748,17 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17361748
workerSetSuccesses[workerId]->fetch_add(1);
17371749
} else {
17381750
workerSetErrors[workerId]->fetch_add(1);
1739-
if (FLAGS_verbose &&
1740-
(workerSetErrors[workerId]->load() % 1000 == 1)) {
1751+
if (FLAGS_verbose && workerSetErrors[workerId]->load() == 1) {
17411752
printf(
1742-
"Benchmark SET error (on GET miss, sample 1/1000): %s\n",
1753+
"Benchmark SET error (on GET miss, first sample): %s\n",
17431754
carbon::resultToString(*setResult.result_ref()));
17441755
}
17451756
}
17461757
} else {
17471758
workerGetErrors[workerId]->fetch_add(1);
1748-
if (FLAGS_verbose && (workerGetErrors[workerId]->load() % 1000 == 1)) {
1759+
if (FLAGS_verbose && workerGetErrors[workerId]->load() == 1) {
17491760
printf(
1750-
"Benchmark GET error (sample 1/1000): %s\n",
1761+
"Benchmark GET error (first sample): %s\n",
17511762
carbon::resultToString(*result.result_ref()));
17521763
}
17531764
}
@@ -1778,7 +1789,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17781789
}
17791790

17801791
// Same pattern as production McrouterAdapter::coro()
1781-
auto [promise, future] = folly::coro::makePromiseContract<UcbSetReply>();
1792+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
17821793

17831794
clientPtr->send(
17841795
request,
@@ -1789,8 +1800,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17891800

17901801
UcbSetReply result;
17911802
try {
1792-
result = co_await folly::coro::timeout(
1793-
std::move(future), std::chrono::seconds(10));
1803+
result = co_await std::move(future).within(std::chrono::seconds(10));
17941804
} catch (const std::exception&) {
17951805
workerTotalOps[workerId]->fetch_add(1);
17961806
workerSetOps[workerId]->fetch_add(1);
@@ -1815,9 +1825,9 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
18151825
workerSetSuccesses[workerId]->fetch_add(1);
18161826
} else {
18171827
workerSetErrors[workerId]->fetch_add(1);
1818-
if (FLAGS_verbose && (workerSetErrors[workerId]->load() % 1000 == 1)) {
1828+
if (FLAGS_verbose && workerSetErrors[workerId]->load() == 1) {
18191829
printf(
1820-
"Benchmark SET error (sample 1/1000): %s\n",
1830+
"Benchmark SET error (first sample): %s\n",
18211831
carbon::resultToString(*result.result_ref()));
18221832
}
18231833
}
@@ -1868,7 +1878,9 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
18681878
co_await folly::futures::sleep(std::chrono::milliseconds(1));
18691879
}
18701880

1871-
co_await scope.joinAsync();
1881+
// Measurement window is over: cancel any still-outstanding requests and
1882+
// join. This drains deterministically instead of hanging on stragglers.
1883+
co_await scope.cancelAndJoinAsync();
18721884

18731885
// No need to sync to global counters - they'll be summed at the end
18741886
co_return;

0 commit comments

Comments
 (0)