Skip to content

Commit 19cc838

Browse files
Lenar Fatikhovmeta-codesync[bot]
authored andcommitted
Stop thread-jumps before shutdown frees the proxies
Summary: `CarbonRouterInstance::shutdownImpl()` calls `proxyEvbs_.clear()`, which destroys the `VirtualEventBase`s one at a time and frees each `Proxy` along with them (`Proxy::createProxy` ties the two together via `runOnDestruction`). The other proxy IO threads are still routing at that point -- the join comes later -- and `proxies_` is a vector of raw `Proxy*` that is never cleared. So a request that thread-jumps on a live proxy can resolve a target that `clear()` has already freed. Confirmed on two coredumps (aarch64 and x86-64) from the `fbpkg` / `fbpkg.fetch` CLIs, which stand up and tear down a router on every invocation: mov (%rcx,%rdx,8),%rax ; rax = proxies_[idx] -- ok mov 0x18(%rax),%rbx ; rbx = Proxy->eventBase_ -- ok, read 0 ... mov 0x18(%rbx),%rcx ; SIGSEGV at 0x18, rbx == 0 `proxies_[i]` was still readable, but `eventBase_` -- a reference member that cannot be null in a live `Proxy` -- read 0. The object had been freed and its memory recycled. The x86 faulting `rip` matches the stack reported in S688594. Two halves, because there are two populations of dangerous hops: - New hops. `proxiesDraining_` is set at the start of proxy teardown (after `joinAuxiliaryThreads()`, before anything is destroyed) and checked by the two accessors that resolve a hop target, `getProxyForThreadJump()` and `getProxyBaseForThreadJump()`. While draining they return nullptr and the caller routes locally -- the existing "stay on this thread" path, which every hop site already handles. - In-flight hops. `fenceProxyEventBases()` round-trips every proxy event base before any `VirtualEventBase` is destroyed. No hop site suspends between resolving a target and enqueueing on it, so a hop that read the flag as false is still running inline on its source proxy's thread, and the fence task queued there cannot run until it finishes. Once every proxy has passed the fence, each pending hop is covered by a keep-alive on its target, and `~VirtualEventBase()` blocks on those. Relaxed ordering is sufficient: the happens-before comes from the fence's notification queue, not from the flag. Cost is one relaxed load of a read-mostly flag per hop resolution, behind a consistent-hash lookup (SRRoute) or a full route-handle traverse (Multi\*Routes). `getProxyFromHash()` is replaced by `getProxyForThreadJump()`; it had one in-tree caller, but was public on an OSS-mirrored header, so this is an API break for the GitHub mirror. Reviewed By: vrishal, stuclar Differential Revision: D115935603 fbshipit-source-id: 3927b8d85512262cf222b106e6aa3d8230cee68f
1 parent c2b8413 commit 19cc838

3 files changed

Lines changed: 177 additions & 2 deletions

File tree

mcrouter/CarbonRouterInstance-inl.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,17 @@ CarbonRouterInstance<RouterInfo>::CarbonRouterInstance(
403403
McrouterOptions inputOptions)
404404
: CarbonRouterInstanceBase(std::move(inputOptions)) {}
405405

406+
template <class RouterInfo>
407+
void CarbonRouterInstance<RouterInfo>::fenceProxyEventBases() noexcept {
408+
// Serial: proxyEvbs_.clear() pays the same N round-trips and dominates.
409+
for (const auto& proxyEvb : proxyEvbs_) {
410+
CHECK(!proxyEvb->getEventBase().inRunningEventBaseThread())
411+
<< "CarbonRouterInstance shutdown must not run on a proxy event base "
412+
"thread";
413+
proxyEvb->getEventBase().runInEventBaseThreadAndWait([] {});
414+
}
415+
}
416+
406417
template <class RouterInfo>
407418
void CarbonRouterInstance<RouterInfo>::shutdownImpl() noexcept {
408419
joinAuxiliaryThreads();
@@ -412,6 +423,9 @@ void CarbonRouterInstance<RouterInfo>::shutdownImpl() noexcept {
412423
destinationMap->disableProbes();
413424
});
414425
}
426+
// Both must precede clear(), which frees proxies other threads still use.
427+
setProxiesDraining();
428+
fenceProxyEventBases();
415429
proxyEvbs_.clear();
416430
resetMetadata();
417431
resetAxonProxyClientFactory();

mcrouter/CarbonRouterInstance.h

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#include <thread>
1616
#include <vector>
1717

18+
#include <folly/CppAttributes.h>
19+
#include <folly/Likely.h>
1820
#include <folly/Range.h>
1921
#include <folly/executors/IOThreadPoolExecutor.h>
2022

@@ -241,8 +243,17 @@ class CarbonRouterInstance
241243
cpuStatsWorker_.reset();
242244
}
243245

244-
Proxy<RouterInfo>& getProxyFromHash(size_t hash) {
245-
return *proxies_[hash % proxies_.size()];
246+
/**
247+
* Returns nullptr while the router is draining; route locally instead.
248+
* Must be called from a proxy event base thread, and the result must not
249+
* outlive the current turn of that thread's loop.
250+
*/
251+
Proxy<RouterInfo>* FOLLY_NULLABLE getProxyFromHash(size_t hash) const {
252+
if (FOLLY_UNLIKELY(proxiesDraining_.load(std::memory_order_relaxed))) {
253+
return nullptr;
254+
}
255+
DCHECK(!proxies_.empty());
256+
return proxies_[hash % proxies_.size()];
246257
}
247258

248259
CarbonRouterInstance(const CarbonRouterInstance&) = delete;
@@ -267,6 +278,13 @@ class CarbonRouterInstance
267278

268279
std::atomic<bool> shutdownStarted_{false};
269280

281+
// Relaxed: the happens-before comes from fenceProxyEventBases(), not this.
282+
std::atomic<bool> proxiesDraining_{false};
283+
284+
void setProxiesDraining() noexcept {
285+
proxiesDraining_.store(true, std::memory_order_relaxed);
286+
}
287+
270288
FileObserverHandle runtimeVarsObserverHandle_;
271289

272290
ConfigApi::CallbackHandle configUpdateHandle_;
@@ -305,6 +323,10 @@ class CarbonRouterInstance
305323
folly::Expected<folly::Unit, std::string> setupProxy(
306324
const std::vector<folly::EventBase*>& evbs);
307325

326+
// Waits for every proxy event base to cycle. Not callable from a proxy
327+
// thread.
328+
void fenceProxyEventBases() noexcept;
329+
308330
void spawnAuxiliaryThreads();
309331
void joinAuxiliaryThreads() noexcept;
310332
void shutdownImpl() noexcept;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <atomic>
9+
#include <chrono>
10+
#include <future>
11+
#include <memory>
12+
#include <thread>
13+
14+
#include <gtest/gtest.h>
15+
16+
#include <folly/ScopeGuard.h>
17+
18+
#include "mcrouter/CarbonRouterInstance.h"
19+
// defaultTestOptions(); its own header does not build when included directly.
20+
#include "mcrouter/options.h"
21+
#include "mcrouter/routes/McrouterRouteHandle.h"
22+
23+
using namespace facebook::memcache;
24+
using namespace facebook::memcache::mcrouter;
25+
26+
namespace {
27+
28+
// One thread to hold hostage, one to observe.
29+
constexpr size_t kNumProxies = 2;
30+
31+
// create(), not init(): each test must own and tear down its own router.
32+
std::shared_ptr<CarbonRouterInstance<McrouterRouterInfo>> makeRouter(
33+
size_t numProxies) {
34+
McrouterOptions opts = defaultTestOptions();
35+
opts.config = "{ \"route\": \"NullRoute\" }";
36+
opts.num_proxies = numProxies;
37+
return CarbonRouterInstance<McrouterRouterInfo>::create(std::move(opts));
38+
}
39+
40+
} // namespace
41+
42+
// Hold proxy 1's thread hostage and check what a hop resolved from it sees.
43+
// Teardown stalls on that thread either way, so the task observes the state
44+
// teardown reached before it needed proxy 1.
45+
TEST(CarbonRouterInstanceShutdownTest, ShutdownDrainsProxiesBeforeFreeingThem) {
46+
auto router = makeRouter(kNumProxies);
47+
ASSERT_NE(router, nullptr);
48+
ASSERT_NE(router->getProxy(1), nullptr);
49+
50+
// Index arithmetic only; off a proxy thread, so do not copy this shape.
51+
for (size_t hash = 0; hash < 2 * kNumProxies; ++hash) {
52+
EXPECT_EQ(
53+
router->getProxyFromHash(hash), router->getProxy(hash % kNumProxies));
54+
}
55+
56+
std::promise<void> taskRunning;
57+
std::future<void> taskRunningFuture = taskRunning.get_future();
58+
std::promise<bool> observed;
59+
std::future<bool> observedFuture = observed.get_future();
60+
61+
router->getProxy(1)->eventBase().getEventBase().runInEventBaseThread(
62+
[&router, &taskRunning, &observed] {
63+
taskRunning.set_value();
64+
bool draining = false;
65+
// Bounded so a regression fails instead of pinning this thread.
66+
for (int i = 0; i < 1000 && !draining; ++i) {
67+
draining = router->getProxyFromHash(0) == nullptr;
68+
if (!draining) {
69+
/* sleep override */
70+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
71+
}
72+
}
73+
observed.set_value(draining);
74+
});
75+
76+
taskRunningFuture.wait();
77+
78+
std::thread shutdownThread([&router] { router->shutdown(); });
79+
SCOPE_EXIT {
80+
shutdownThread.join();
81+
};
82+
83+
EXPECT_TRUE(observedFuture.get())
84+
<< "shutdown() began tearing proxies down without first marking them as "
85+
"draining, so a thread jump could still resolve a proxy that is "
86+
"about to be freed";
87+
}
88+
89+
// No proxy may be destroyed while any proxy thread is still running: a hop
90+
// that resolved a target just before the flag flipped is still inline on its
91+
// own thread. Occupy proxy 1 and watch proxy 0.
92+
TEST(CarbonRouterInstanceShutdownTest, ShutdownFencesBeforeDestroyingAnyProxy) {
93+
auto router = makeRouter(kNumProxies);
94+
ASSERT_NE(router, nullptr);
95+
ASSERT_NE(router->getProxy(0), nullptr);
96+
ASSERT_NE(router->getProxy(1), nullptr);
97+
98+
std::atomic<bool> proxy0Destroyed{false};
99+
router->getProxy(0)->eventBase().runOnDestruction(
100+
[&proxy0Destroyed] { proxy0Destroyed.store(true); });
101+
102+
std::promise<void> taskRunning;
103+
std::future<void> taskRunningFuture = taskRunning.get_future();
104+
std::promise<void> release;
105+
std::shared_future<void> releaseFuture = release.get_future();
106+
107+
router->getProxy(1)->eventBase().getEventBase().runInEventBaseThread(
108+
[&taskRunning, releaseFuture] {
109+
taskRunning.set_value();
110+
releaseFuture.wait();
111+
});
112+
taskRunningFuture.wait();
113+
114+
std::thread shutdownThread([&router] { router->shutdown(); });
115+
116+
// Fallback for early returns; the happy path joins inline below.
117+
bool released = false;
118+
SCOPE_EXIT {
119+
if (!released) {
120+
release.set_value();
121+
shutdownThread.join();
122+
}
123+
};
124+
125+
// Bounded negative wait; can only pass spuriously, never fail spuriously.
126+
// Kept short: this pins a proxy thread and other tests in this binary are
127+
// latency-sensitive. Without the fence, clear() frees proxy 0 immediately.
128+
/* sleep override */
129+
std::this_thread::sleep_for(std::chrono::milliseconds(150));
130+
EXPECT_FALSE(proxy0Destroyed.load())
131+
<< "shutdown() destroyed a proxy while another proxy thread was still "
132+
"running, so a hop that had already resolved that proxy as its target "
133+
"could still be about to use it";
134+
135+
released = true;
136+
release.set_value();
137+
shutdownThread.join();
138+
EXPECT_TRUE(proxy0Destroyed.load()); // not vacuous
139+
}

0 commit comments

Comments
 (0)