Skip to content

Commit db7b5df

Browse files
committed
Merge bitcoin/bitcoin#28551: http: bugfix: allow server shutdown in case of remote client disconnection
68f23f5 http: bugfix: track closed connection (stickies-v) 084d037 http: log connection instead of request count (stickies-v) 41f9027 http: refactor: use encapsulated HTTPRequestTracker (stickies-v) Pull request description: #26742 significantly increased the http server shutdown speed, but also introduced a bug (#27722 - see bitcoin/bitcoin#27722 (comment) for steps to reproduce on master) that causes http server shutdown to halt in case of a remote client disconnection. This happens because `evhttp_request_set_on_complete_cb` is never called and thus the request is never removed from `g_requests`. This PR fixes that bug, and improves robustness of the code by encapsulating the request tracking logic. Earlier approaches (#27909, #27245, #19434) attempted to resolve this but [imo are fundamentally unsafe](bitcoin/bitcoin#27909 (comment)) because of differences in lifetime between an `evhttp_request` and `evhttp_connection`. We don't need to keep track of open requests or connections, we just [need to ensure](bitcoin/bitcoin#19420 (comment)) that there are no active requests on server shutdown. Because a connection can have multiple requests, and a request can be completed in various ways (the request actually being handled, or the client performing a remote disconnect), keeping a counter per connection seems like the approach with the least overhead to me. Fixes #27722 ACKs for top commit: vasild: ACK 68f23f5 theStack: ACK 68f23f5 Tree-SHA512: dfa711ff55ec75ba44d73e9e6fac16b0be25cf3c20868c2145a844a7878ad9fc6998d9ff62d72f3a210bfa411ef03d3757b73d68a7c22926e874c421e51444d6
2 parents 2f835d2 + 68f23f5 commit db7b5df

File tree

1 file changed

+66
-17
lines changed

1 file changed

+66
-17
lines changed

src/httpserver.cpp

+66-17
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <rpc/protocol.h> // For HTTP status codes
1818
#include <shutdown.h>
1919
#include <sync.h>
20+
#include <util/check.h>
2021
#include <util/strencodings.h>
2122
#include <util/threadnames.h>
2223
#include <util/translation.h>
@@ -28,7 +29,7 @@
2829
#include <memory>
2930
#include <optional>
3031
#include <string>
31-
#include <unordered_set>
32+
#include <unordered_map>
3233

3334
#include <sys/types.h>
3435
#include <sys/stat.h>
@@ -149,10 +150,61 @@ static GlobalMutex g_httppathhandlers_mutex;
149150
static std::vector<HTTPPathHandler> pathHandlers GUARDED_BY(g_httppathhandlers_mutex);
150151
//! Bound listening sockets
151152
static std::vector<evhttp_bound_socket *> boundSockets;
153+
154+
/**
155+
* @brief Helps keep track of open `evhttp_connection`s with active `evhttp_requests`
156+
*
157+
*/
158+
class HTTPRequestTracker
159+
{
160+
private:
161+
mutable Mutex m_mutex;
162+
mutable std::condition_variable m_cv;
163+
//! For each connection, keep a counter of how many requests are open
164+
std::unordered_map<const evhttp_connection*, size_t> m_tracker GUARDED_BY(m_mutex);
165+
166+
void RemoveConnectionInternal(const decltype(m_tracker)::iterator it) EXCLUSIVE_LOCKS_REQUIRED(m_mutex)
167+
{
168+
m_tracker.erase(it);
169+
if (m_tracker.empty()) m_cv.notify_all();
170+
}
171+
public:
172+
//! Increase request counter for the associated connection by 1
173+
void AddRequest(evhttp_request* req) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
174+
{
175+
const evhttp_connection* conn{Assert(evhttp_request_get_connection(Assert(req)))};
176+
WITH_LOCK(m_mutex, ++m_tracker[conn]);
177+
}
178+
//! Decrease request counter for the associated connection by 1, remove connection if counter is 0
179+
void RemoveRequest(evhttp_request* req) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
180+
{
181+
const evhttp_connection* conn{Assert(evhttp_request_get_connection(Assert(req)))};
182+
LOCK(m_mutex);
183+
auto it{m_tracker.find(conn)};
184+
if (it != m_tracker.end() && it->second > 0) {
185+
if (--(it->second) == 0) RemoveConnectionInternal(it);
186+
}
187+
}
188+
//! Remove a connection entirely
189+
void RemoveConnection(const evhttp_connection* conn) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
190+
{
191+
LOCK(m_mutex);
192+
auto it{m_tracker.find(Assert(conn))};
193+
if (it != m_tracker.end()) RemoveConnectionInternal(it);
194+
}
195+
size_t CountActiveConnections() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
196+
{
197+
return WITH_LOCK(m_mutex, return m_tracker.size());
198+
}
199+
//! Wait until there are no more connections with active requests in the tracker
200+
void WaitUntilEmpty() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
201+
{
202+
WAIT_LOCK(m_mutex, lock);
203+
m_cv.wait(lock, [this]() EXCLUSIVE_LOCKS_REQUIRED(m_mutex) { return m_tracker.empty(); });
204+
}
205+
};
152206
//! Track active requests
153-
static GlobalMutex g_requests_mutex;
154-
static std::condition_variable g_requests_cv;
155-
static std::unordered_set<evhttp_request*> g_requests GUARDED_BY(g_requests_mutex);
207+
static HTTPRequestTracker g_requests;
156208

157209
/** Check if a network address is allowed to access the HTTP server */
158210
static bool ClientAllowed(const CNetAddr& netaddr)
@@ -210,22 +262,22 @@ std::string RequestMethodString(HTTPRequest::RequestMethod m)
210262
/** HTTP request callback */
211263
static void http_request_cb(struct evhttp_request* req, void* arg)
212264
{
213-
// Track requests and notify when a request is completed.
265+
evhttp_connection* conn{evhttp_request_get_connection(req)};
266+
// Track active requests
214267
{
215-
WITH_LOCK(g_requests_mutex, g_requests.insert(req));
216-
g_requests_cv.notify_all();
268+
g_requests.AddRequest(req);
217269
evhttp_request_set_on_complete_cb(req, [](struct evhttp_request* req, void*) {
218-
auto n{WITH_LOCK(g_requests_mutex, return g_requests.erase(req))};
219-
assert(n == 1);
220-
g_requests_cv.notify_all();
270+
g_requests.RemoveRequest(req);
271+
}, nullptr);
272+
evhttp_connection_set_closecb(conn, [](evhttp_connection* conn, void* arg) {
273+
g_requests.RemoveConnection(conn);
221274
}, nullptr);
222275
}
223276

224277
// Disable reading to work around a libevent bug, fixed in 2.1.9
225278
// See https://github.com/libevent/libevent/commit/5ff8eb26371c4dc56f384b2de35bea2d87814779
226279
// and https://github.com/bitcoin/bitcoin/pull/11593.
227280
if (event_get_version_number() >= 0x02010600 && event_get_version_number() < 0x02010900) {
228-
evhttp_connection* conn = evhttp_request_get_connection(req);
229281
if (conn) {
230282
bufferevent* bev = evhttp_connection_get_bufferevent(conn);
231283
if (bev) {
@@ -473,13 +525,10 @@ void StopHTTPServer()
473525
}
474526
boundSockets.clear();
475527
{
476-
WAIT_LOCK(g_requests_mutex, lock);
477-
if (!g_requests.empty()) {
478-
LogPrint(BCLog::HTTP, "Waiting for %d requests to stop HTTP server\n", g_requests.size());
528+
if (const auto n_connections{g_requests.CountActiveConnections()}; n_connections != 0) {
529+
LogPrint(BCLog::HTTP, "Waiting for %d connections to stop HTTP server\n", n_connections);
479530
}
480-
g_requests_cv.wait(lock, []() EXCLUSIVE_LOCKS_REQUIRED(g_requests_mutex) {
481-
return g_requests.empty();
482-
});
531+
g_requests.WaitUntilEmpty();
483532
}
484533
if (eventHTTP) {
485534
// Schedule a callback to call evhttp_free in the event base thread, so

0 commit comments

Comments
 (0)