Skip to content

Commit 6023bbb

Browse files
committed
ipc: drain in-flight server calls before shutdown frees node state
Fix bitcoin#35845, an assertion failure in MinerImpl::chainman() during shutdown of an IPC-mining node. Shutdown() calls disconnectIncoming() before node.chainman.reset(). Disconnecting cancels the KJ promise of an in-flight IPC server call, but a C++ server method body already dispatched to a libmultiprocess worker thread is not interrupted and runs to completion. A still-running body (an in-flight Mining.checkBlock) could then dereference m_node.chainman after chainman.reset() nulled it, aborting on Assert(m_node.chainman). Make disconnectIncoming() disconnect the non-parent incoming connections, wait off the event loop thread for their in-flight server call bodies to finish (Connection::waitDrained), and only then destroy them and return, so Shutdown() frees node state only once no server code is running. Log when the wait actually blocks so a shutdown hang here is diagnosable. No wait is needed for calls parked in waitTipChanged()/waitNext(): Interrupt() runs before Shutdown() and notifies m_tip_block_cv after setting the shutdown signal, so those return before disconnectIncoming() runs. Intentional limitations, to keep the fix narrow: m_impl destructors scheduled on the async cleanup thread are not waited for, the kept-open parent connection is not drained, and new incoming connections can still be accepted during shutdown (preventing that needs a listener API, proposed in bitcoin-core/libmultiprocess#269). This change was written with Claude Fable 5.
1 parent 44c151e commit 6023bbb

1 file changed

Lines changed: 47 additions & 2 deletions

File tree

‎src/ipc/capnp/protocol.cpp‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <sys/socket.h>
2727
#include <system_error>
2828
#include <thread>
29+
#include <vector>
2930

3031
namespace ipc {
3132
namespace capnp {
@@ -108,12 +109,56 @@ class CapnpProtocol : public Protocol
108109
void disconnectIncoming() override
109110
{
110111
if (!m_loop) return;
111-
// Delete incoming connections, except the connection to a parent
112+
// Disconnect incoming connections, except the connection to a parent
112113
// process (if there is one), since a parent process should be able to
113114
// monitor and control this process, even during shutdown.
115+
//
116+
// Disconnecting cancels the KJ promise of any call in progress, but a
117+
// server method body that was already dispatched to a worker thread is
118+
// NOT interrupted and runs to completion. So before destroying the
119+
// connections and returning, wait for in-flight call bodies to finish
120+
// (mp::Connection::waitDrained). Without this wait, a still-running
121+
// body (e.g. Mining.checkBlock) could dereference node state that the
122+
// caller (Shutdown) frees right after this function returns, causing
123+
// https://github.com/bitcoin/bitcoin/issues/35845.
124+
//
125+
// The wait happens off the event loop thread, between two sync()
126+
// calls: call bodies need the event loop to deliver their results, so
127+
// blocking the loop until they finish would deadlock.
128+
//
129+
// Limitations, intentional to keep this narrow (see #35845 discussion):
130+
// - Only server call *bodies* are drained. ProxyServer m_impl
131+
// destructors scheduled via EventLoop::addAsyncCleanup() run later
132+
// on the async cleanup thread and are not waited for. (One of
133+
// several reasons IPC object implementations should not have
134+
// nontrivial destructors.)
135+
// - Only the removed connections are drained; the kept-open parent
136+
// connection is not (that would require pausing new calls on it).
137+
// - New incoming connections can still be accepted while this runs;
138+
// preventing that requires an API to stop the listener, proposed in
139+
// https://github.com/bitcoin-core/libmultiprocess/pull/269.
140+
std::vector<mp::Connection::Tracker> disconnected;
114141
m_loop->sync([&] {
115-
m_loop->m_incoming_connections.remove_if([this](mp::Connection& c) { return &c != m_parent_connection; });
142+
for (mp::Connection& c : m_loop->incomingConnections()) {
143+
if (&c != m_parent_connection) {
144+
disconnected.push_back(c.tracker());
145+
c.disconnect();
146+
}
147+
}
116148
});
149+
// After disconnect(), the remaining counted server objects are exactly
150+
// the ones kept alive by in-flight calls (idle ones are garbage
151+
// collected synchronously by the disconnect), so a nonzero count means
152+
// the wait below will actually block. Log when that happens so a
153+
// shutdown hang here is diagnosable.
154+
size_t pending{0};
155+
for (const auto& connection : disconnected) pending += connection->pendingServerObjects();
156+
if (pending > 0) {
157+
LogInfo("ipc: Waiting for %d server object(s) in use by in-flight IPC call(s) on %d connection(s) to be freed",
158+
pending, disconnected.size());
159+
}
160+
for (const auto& tracker : disconnected) tracker->waitDrained();
161+
if (pending > 0) LogInfo("ipc: Incoming IPC connections drained");
117162
}
118163
mp::Stream makeStream(mp::SocketId socket) override
119164
{

0 commit comments

Comments
 (0)