Skip to content

Commit 82cd721

Browse files
charles-typfacebook-github-bot
authored andcommitted
Fix completion-hang deadlock so all clients report BENCHMARK_DONE (#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. Reviewed By: excelle08 Differential Revision: D110091576
1 parent be3669a commit 82cd721

1 file changed

Lines changed: 44 additions & 31 deletions

File tree

packages/ucache_bench/client/UcacheBenchClient.cpp

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
#include <folly/coro/BlockingWait.h>
2525
#include <folly/coro/Promise.h>
2626
#include <folly/coro/Timeout.h>
27+
#include <folly/fibers/FiberManagerMap.h>
28+
#include <folly/futures/Future.h>
29+
#include <folly/futures/Promise.h>
2730
#include <folly/io/async/ScopedEventBaseThread.h>
2831
#include <folly/portability/GFlags.h>
2932
#include <mcrouter/McrouterFiberContext.h>
@@ -987,8 +990,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
987990
request.value_ref() = *folly::IOBuf::copyBuffer(value);
988991
request.exptime_ref() = 3600;
989992

990-
auto [promise, future] =
991-
folly::coro::makePromiseContract<UcbSetReply>();
993+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
992994

993995
clientPtr->send(
994996
request,
@@ -998,8 +1000,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
9981000
});
9991001

10001002
try {
1001-
co_await folly::coro::timeout(
1002-
std::move(future), std::chrono::seconds(10));
1003+
co_await std::move(future).within(std::chrono::seconds(10));
10031004
scanOps++;
10041005
} catch (const std::exception&) {
10051006
// Request timed out or failed (e.g. send() returned false and the
@@ -1103,7 +1104,9 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11031104
auto warmupWorker =
11041105
[&](memcache::mcrouter::CarbonRouterClient<UcacheBenchRouterInfo>*
11051106
clientPtr) -> folly::coro::Task<void> {
1106-
folly::coro::AsyncScope scope;
1107+
// CancellableAsyncScope so stragglers at warmup-end are cancelled, not
1108+
// waited on forever (same hang as the measurement worker — see below).
1109+
folly::coro::CancellableAsyncScope scope;
11071110
auto exe = co_await folly::coro::co_current_executor;
11081111

11091112
std::atomic<uint64_t> inflight{0};
@@ -1137,7 +1140,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11371140
}
11381141

11391142
// Same pattern as production McrouterAdapter::coro()
1140-
auto [promise, future] = folly::coro::makePromiseContract<UcbSetReply>();
1143+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
11411144

11421145
clientPtr->send(
11431146
request,
@@ -1148,8 +1151,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11481151

11491152
UcbSetReply result;
11501153
try {
1151-
result = co_await folly::coro::timeout(
1152-
std::move(future), std::chrono::seconds(10));
1154+
result = co_await std::move(future).within(std::chrono::seconds(10));
11531155
} catch (const std::exception&) {
11541156
// Request never completed (send() rejected without callback, or stuck
11551157
// pre-send during the connection storm). Count as error and return so
@@ -1165,11 +1167,15 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11651167
localSuccesses++;
11661168
} else {
11671169
localErrors++;
1168-
// Rate-limit verbose error output to avoid flooding stdout pipe
1169-
// which can deadlock when run through benchpress/automark
1170-
if (FLAGS_verbose && (localErrors.load() % 1000 == 1)) {
1170+
// Print only the FIRST error per worker. Any periodic sampling (even
1171+
// 1/1000) floods stdout under a TKO storm at multi-M QPS; the
1172+
// synchronous write() blocks on the backpressured benchpress/automark
1173+
// pipe while holding the stdio FILE lock and glog's log_mutex, wedging
1174+
// every worker thread -> joinAsync never returns -> BENCHMARK_DONE is
1175+
// never sent.
1176+
if (FLAGS_verbose && localErrors.load() == 1) {
11711177
printf(
1172-
"Warmup SET error (sample 1/1000): %s\n",
1178+
"Warmup SET error (first sample): %s\n",
11731179
carbon::resultToString(*result.result_ref()));
11741180
}
11751181
}
@@ -1220,7 +1226,8 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
12201226
co_await folly::futures::sleep(std::chrono::milliseconds(1));
12211227
}
12221228

1223-
co_await scope.joinAsync();
1229+
// Warmup window over: cancel stragglers and join (deterministic drain).
1230+
co_await scope.cancelAndJoinAsync();
12241231

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

16301644
// Send one GET request - matches production McrouterAdapter::coro() pattern
@@ -1647,7 +1661,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16471661
}
16481662

16491663
// Same pattern as production McrouterAdapter::coro()
1650-
auto [promise, future] = folly::coro::makePromiseContract<UcbGetReply>();
1664+
auto [promise, future] = folly::makePromiseContract<UcbGetReply>();
16511665

16521666
clientPtr->send(
16531667
request,
@@ -1658,8 +1672,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16581672

16591673
UcbGetReply result;
16601674
try {
1661-
result = co_await folly::coro::timeout(
1662-
std::move(future), std::chrono::seconds(10));
1675+
result = co_await std::move(future).within(std::chrono::seconds(10));
16631676
} catch (const std::exception&) {
16641677
// Request never completed; count as a GET error and return so the
16651678
// measurement worker's scope.joinAsync() can't hang.
@@ -1708,7 +1721,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17081721
}
17091722

17101723
auto [setPromise, setFuture] =
1711-
folly::coro::makePromiseContract<UcbSetReply>();
1724+
folly::makePromiseContract<UcbSetReply>();
17121725

17131726
clientPtr->send(
17141727
setRequest,
@@ -1719,8 +1732,8 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17191732

17201733
UcbSetReply setResult;
17211734
try {
1722-
setResult = co_await folly::coro::timeout(
1723-
std::move(setFuture), std::chrono::seconds(10));
1735+
setResult =
1736+
co_await std::move(setFuture).within(std::chrono::seconds(10));
17241737
} catch (const std::exception&) {
17251738
workerSetOps[workerId]->fetch_add(1);
17261739
workerSetErrors[workerId]->fetch_add(1);
@@ -1732,18 +1745,17 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17321745
workerSetSuccesses[workerId]->fetch_add(1);
17331746
} else {
17341747
workerSetErrors[workerId]->fetch_add(1);
1735-
if (FLAGS_verbose &&
1736-
(workerSetErrors[workerId]->load() % 1000 == 1)) {
1748+
if (FLAGS_verbose && workerSetErrors[workerId]->load() == 1) {
17371749
printf(
1738-
"Benchmark SET error (on GET miss, sample 1/1000): %s\n",
1750+
"Benchmark SET error (on GET miss, first sample): %s\n",
17391751
carbon::resultToString(*setResult.result_ref()));
17401752
}
17411753
}
17421754
} else {
17431755
workerGetErrors[workerId]->fetch_add(1);
1744-
if (FLAGS_verbose && (workerGetErrors[workerId]->load() % 1000 == 1)) {
1756+
if (FLAGS_verbose && workerGetErrors[workerId]->load() == 1) {
17451757
printf(
1746-
"Benchmark GET error (sample 1/1000): %s\n",
1758+
"Benchmark GET error (first sample): %s\n",
17471759
carbon::resultToString(*result.result_ref()));
17481760
}
17491761
}
@@ -1774,7 +1786,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17741786
}
17751787

17761788
// Same pattern as production McrouterAdapter::coro()
1777-
auto [promise, future] = folly::coro::makePromiseContract<UcbSetReply>();
1789+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
17781790

17791791
clientPtr->send(
17801792
request,
@@ -1785,8 +1797,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17851797

17861798
UcbSetReply result;
17871799
try {
1788-
result = co_await folly::coro::timeout(
1789-
std::move(future), std::chrono::seconds(10));
1800+
result = co_await std::move(future).within(std::chrono::seconds(10));
17901801
} catch (const std::exception&) {
17911802
workerTotalOps[workerId]->fetch_add(1);
17921803
workerSetOps[workerId]->fetch_add(1);
@@ -1811,9 +1822,9 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
18111822
workerSetSuccesses[workerId]->fetch_add(1);
18121823
} else {
18131824
workerSetErrors[workerId]->fetch_add(1);
1814-
if (FLAGS_verbose && (workerSetErrors[workerId]->load() % 1000 == 1)) {
1825+
if (FLAGS_verbose && workerSetErrors[workerId]->load() == 1) {
18151826
printf(
1816-
"Benchmark SET error (sample 1/1000): %s\n",
1827+
"Benchmark SET error (first sample): %s\n",
18171828
carbon::resultToString(*result.result_ref()));
18181829
}
18191830
}
@@ -1864,7 +1875,9 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
18641875
co_await folly::futures::sleep(std::chrono::milliseconds(1));
18651876
}
18661877

1867-
co_await scope.joinAsync();
1878+
// Measurement window is over: cancel any still-outstanding requests and
1879+
// join. This drains deterministically instead of hanging on stragglers.
1880+
co_await scope.cancelAndJoinAsync();
18681881

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

0 commit comments

Comments
 (0)