Skip to content

Commit b80922d

Browse files
charles-typmeta-codesync[bot]
authored andcommitted
Fix completion-hang deadlock so all clients report BENCHMARK_DONE
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 876c88a commit b80922d

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>
@@ -993,8 +995,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
993995
request.value_ref() = *folly::IOBuf::copyBuffer(value);
994996
request.exptime_ref() = 3600;
995997

996-
auto [promise, future] =
997-
folly::coro::makePromiseContract<UcbSetReply>();
998+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
998999

9991000
clientPtr->send(
10001001
request,
@@ -1004,8 +1005,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
10041005
});
10051006

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

11121114
std::atomic<uint64_t> inflight{0};
@@ -1140,7 +1142,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11401142
}
11411143

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

11451147
clientPtr->send(
11461148
request,
@@ -1151,8 +1153,7 @@ UcacheBenchClient::WarmupResults UcacheBenchClient::warmup() {
11511153

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

1226-
co_await scope.joinAsync();
1231+
// Warmup window over: cancel stragglers and join (deterministic drain).
1232+
co_await scope.cancelAndJoinAsync();
12271233

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

16331646
// Send one GET request - matches production McrouterAdapter::coro() pattern
@@ -1650,7 +1663,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16501663
}
16511664

16521665
// Same pattern as production McrouterAdapter::coro()
1653-
auto [promise, future] = folly::coro::makePromiseContract<UcbGetReply>();
1666+
auto [promise, future] = folly::makePromiseContract<UcbGetReply>();
16541667

16551668
clientPtr->send(
16561669
request,
@@ -1661,8 +1674,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
16611674

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

17131725
auto [setPromise, setFuture] =
1714-
folly::coro::makePromiseContract<UcbSetReply>();
1726+
folly::makePromiseContract<UcbSetReply>();
17151727

17161728
clientPtr->send(
17171729
setRequest,
@@ -1722,8 +1734,8 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17221734

17231735
UcbSetReply setResult;
17241736
try {
1725-
setResult = co_await folly::coro::timeout(
1726-
std::move(setFuture), std::chrono::seconds(10));
1737+
setResult =
1738+
co_await std::move(setFuture).within(std::chrono::seconds(10));
17271739
} catch (const std::exception&) {
17281740
workerSetOps[workerId]->fetch_add(1);
17291741
workerSetErrors[workerId]->fetch_add(1);
@@ -1735,18 +1747,17 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17351747
workerSetSuccesses[workerId]->fetch_add(1);
17361748
} else {
17371749
workerSetErrors[workerId]->fetch_add(1);
1738-
if (FLAGS_verbose &&
1739-
(workerSetErrors[workerId]->load() % 1000 == 1)) {
1750+
if (FLAGS_verbose && workerSetErrors[workerId]->load() == 1) {
17401751
printf(
1741-
"Benchmark SET error (on GET miss, sample 1/1000): %s\n",
1752+
"Benchmark SET error (on GET miss, first sample): %s\n",
17421753
carbon::resultToString(*setResult.result_ref()));
17431754
}
17441755
}
17451756
} else {
17461757
workerGetErrors[workerId]->fetch_add(1);
1747-
if (FLAGS_verbose && (workerGetErrors[workerId]->load() % 1000 == 1)) {
1758+
if (FLAGS_verbose && workerGetErrors[workerId]->load() == 1) {
17481759
printf(
1749-
"Benchmark GET error (sample 1/1000): %s\n",
1760+
"Benchmark GET error (first sample): %s\n",
17501761
carbon::resultToString(*result.result_ref()));
17511762
}
17521763
}
@@ -1777,7 +1788,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17771788
}
17781789

17791790
// Same pattern as production McrouterAdapter::coro()
1780-
auto [promise, future] = folly::coro::makePromiseContract<UcbSetReply>();
1791+
auto [promise, future] = folly::makePromiseContract<UcbSetReply>();
17811792

17821793
clientPtr->send(
17831794
request,
@@ -1788,8 +1799,7 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
17881799

17891800
UcbSetReply result;
17901801
try {
1791-
result = co_await folly::coro::timeout(
1792-
std::move(future), std::chrono::seconds(10));
1802+
result = co_await std::move(future).within(std::chrono::seconds(10));
17931803
} catch (const std::exception&) {
17941804
workerTotalOps[workerId]->fetch_add(1);
17951805
workerSetOps[workerId]->fetch_add(1);
@@ -1814,9 +1824,9 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
18141824
workerSetSuccesses[workerId]->fetch_add(1);
18151825
} else {
18161826
workerSetErrors[workerId]->fetch_add(1);
1817-
if (FLAGS_verbose && (workerSetErrors[workerId]->load() % 1000 == 1)) {
1827+
if (FLAGS_verbose && workerSetErrors[workerId]->load() == 1) {
18181828
printf(
1819-
"Benchmark SET error (sample 1/1000): %s\n",
1829+
"Benchmark SET error (first sample): %s\n",
18201830
carbon::resultToString(*result.result_ref()));
18211831
}
18221832
}
@@ -1867,7 +1877,9 @@ UcacheBenchClient::BenchmarkResults UcacheBenchClient::runBenchmark() {
18671877
co_await folly::futures::sleep(std::chrono::milliseconds(1));
18681878
}
18691879

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

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

0 commit comments

Comments
 (0)