Skip to content

Commit a88bd31

Browse files
committed
Merge #14670: http: Fix HTTP server shutdown
28479f9 qa: Test bitcond shutdown (João Barbosa) 8d3f46e http: Remove timeout to exit event loop (João Barbosa) e98a9ee http: Remove unnecessary event_base_loopexit call (João Barbosa) 6b13580 http: Unlisten sockets after all workers quit (João Barbosa) 18e9685 http: Send "Connection: close" header if shutdown is requested (João Barbosa) 02e1e4e rpc: Add wait argument to stop (João Barbosa) Pull request description: Fixes #11777. Reverts #11006. Replaces #13501. With this change the HTTP server will exit gracefully, meaning that all requests will finish processing and sending the response, even if this means to wait more than 2 seconds (current time allowed to exit the event loop). Another small change is that connections are accepted even when the server is stopping, but HTTP requests are rejected. This can be improved later, especially if chunked replies are implemented. Briefly, before this PR, this is the order or events when a request arrives (RPC `stop`): 1. `bufferevent_disable(..., EV_READ)` 2. `StartShutdown()` 3. `evhttp_del_accept_socket(...)` 4. `ThreadHTTP` terminates (event loop exits) because there are no active or pending events thanks to 1. and 3. 5. client doesn't get the response thanks to 4. This can be verified by applying ```diff // Event loop will exit after current HTTP requests have been handled, so // this reply will get back to the client. StartShutdown(); + MilliSleep(2000); return "Bitcoin server stopping"; } ``` and checking the log output: ``` Received a POST request for / from 127.0.0.1:62443 ThreadRPCServer method=stop user=__cookie__ Interrupting HTTP server ** Exited http event loop Interrupting HTTP RPC server Interrupting RPC tor: Thread interrupt Shutdown: In progress... torcontrol thread exit Stopping HTTP RPC server addcon thread exit opencon thread exit Unregistering HTTP handler for / (exactmatch 1) Unregistering HTTP handler for /wallet/ (exactmatch 0) Stopping RPC RPC stopped. Stopping HTTP server Waiting for HTTP worker threads to exit msghand thread exit net thread exit ... sleep 2 seconds ... Waiting for HTTP event thread to exit Stopped HTTP server ``` For this reason point 3. is moved right after all HTTP workers quit. In that moment HTTP replies are queued in the event loop which keeps spinning util all connections are closed. In order to trigger the server side close with keep alive connections (implicit in HTTP/1.1) the header `Connection: close` is sent if shutdown was requested. This can be tested by ``` bitcoind -regtest nc localhost 18443 POST / HTTP/1.1 Authorization: Basic ... Content-Type: application/json Content-Length: 44 {"jsonrpc": "2.0","method":"stop","id":123} ``` Summing up, this PR: - removes explicit event loop exit — event loop exits once there are no active or pending events - changes the moment the listening sockets are removed — explained above - sends header `Connection: close` on active requests when shutdown was requested which is relevant when it's a persistent connection (default in HTTP 1.1) — libevent is aware of this header and closes the connection gracefully - removes event loop explicit break after 2 seconds timeout Tree-SHA512: 4dac1e86abe388697c1e2dedbf31fb36a394cfafe5e64eadbf6ed01d829542785a8c3b91d1ab680d3f03f912d14fc87176428041141441d25dcb6c98a1e069d8
2 parents 4987cdd + 28479f9 commit a88bd31

File tree

7 files changed

+54
-28
lines changed

7 files changed

+54
-28
lines changed

src/httpserver.cpp

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <util/strencodings.h>
1111
#include <netbase.h>
1212
#include <rpc/protocol.h> // For HTTP status codes
13+
#include <shutdown.h>
1314
#include <sync.h>
1415
#include <ui_interface.h>
1516

@@ -21,7 +22,6 @@
2122
#include <sys/types.h>
2223
#include <sys/stat.h>
2324
#include <signal.h>
24-
#include <future>
2525

2626
#include <event2/thread.h>
2727
#include <event2/buffer.h>
@@ -421,17 +421,14 @@ bool UpdateHTTPServerLogging(bool enable) {
421421
}
422422

423423
std::thread threadHTTP;
424-
std::future<bool> threadResult;
425424
static std::vector<std::thread> g_thread_http_workers;
426425

427426
void StartHTTPServer()
428427
{
429428
LogPrint(BCLog::HTTP, "Starting HTTP server\n");
430429
int rpcThreads = std::max((long)gArgs.GetArg("-rpcthreads", DEFAULT_HTTP_THREADS), 1L);
431430
LogPrintf("HTTP: starting %d worker threads\n", rpcThreads);
432-
std::packaged_task<bool(event_base*)> task(ThreadHTTP);
433-
threadResult = task.get_future();
434-
threadHTTP = std::thread(std::move(task), eventBase);
431+
threadHTTP = std::thread(ThreadHTTP, eventBase);
435432

436433
for (int i = 0; i < rpcThreads; i++) {
437434
g_thread_http_workers.emplace_back(HTTPWorkQueueRun, workQueue);
@@ -442,10 +439,6 @@ void InterruptHTTPServer()
442439
{
443440
LogPrint(BCLog::HTTP, "Interrupting HTTP server\n");
444441
if (eventHTTP) {
445-
// Unlisten sockets
446-
for (evhttp_bound_socket *socket : boundSockets) {
447-
evhttp_del_accept_socket(eventHTTP, socket);
448-
}
449442
// Reject requests on current connections
450443
evhttp_set_gencb(eventHTTP, http_reject_request_cb, nullptr);
451444
}
@@ -465,20 +458,14 @@ void StopHTTPServer()
465458
delete workQueue;
466459
workQueue = nullptr;
467460
}
461+
// Unlisten sockets, these are what make the event loop running, which means
462+
// that after this and all connections are closed the event loop will quit.
463+
for (evhttp_bound_socket *socket : boundSockets) {
464+
evhttp_del_accept_socket(eventHTTP, socket);
465+
}
466+
boundSockets.clear();
468467
if (eventBase) {
469468
LogPrint(BCLog::HTTP, "Waiting for HTTP event thread to exit\n");
470-
// Exit the event loop as soon as there are no active events.
471-
event_base_loopexit(eventBase, nullptr);
472-
// Give event loop a few seconds to exit (to send back last RPC responses), then break it
473-
// Before this was solved with event_base_loopexit, but that didn't work as expected in
474-
// at least libevent 2.0.21 and always introduced a delay. In libevent
475-
// master that appears to be solved, so in the future that solution
476-
// could be used again (if desirable).
477-
// (see discussion in https://github.com/bitcoin/bitcoin/pull/6990)
478-
if (threadResult.valid() && threadResult.wait_for(std::chrono::milliseconds(2000)) == std::future_status::timeout) {
479-
LogPrintf("HTTP event loop did not exit within allotted time, sending loopbreak\n");
480-
event_base_loopbreak(eventBase);
481-
}
482469
threadHTTP.join();
483470
}
484471
if (eventHTTP) {
@@ -583,6 +570,9 @@ void HTTPRequest::WriteHeader(const std::string& hdr, const std::string& value)
583570
void HTTPRequest::WriteReply(int nStatus, const std::string& strReply)
584571
{
585572
assert(!replySent && req);
573+
if (ShutdownRequested()) {
574+
WriteHeader("Connection", "close");
575+
}
586576
// Send event to main http thread to send reply message
587577
struct evbuffer* evb = evhttp_request_get_output_buffer(req);
588578
assert(evb);

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
162162
{ "rescanblockchain", 1, "stop_height"},
163163
{ "createwallet", 1, "disable_private_keys"},
164164
{ "getnodeaddresses", 0, "count"},
165+
{ "stop", 0, "wait" },
165166
};
166167
// clang-format on
167168

src/rpc/server.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ UniValue help(const JSONRPCRequest& jsonRequest)
221221
UniValue stop(const JSONRPCRequest& jsonRequest)
222222
{
223223
// Accept the deprecated and ignored 'detach' boolean argument
224+
// Also accept the hidden 'wait' integer argument (milliseconds)
225+
// For instance, 'stop 1000' makes the call wait 1 second before returning
226+
// to the client (intended for testing)
224227
if (jsonRequest.fHelp || jsonRequest.params.size() > 1)
225228
throw std::runtime_error(
226229
RPCHelpMan{"stop",
@@ -229,6 +232,9 @@ UniValue stop(const JSONRPCRequest& jsonRequest)
229232
// Event loop will exit after current HTTP requests have been handled, so
230233
// this reply will get back to the client.
231234
StartShutdown();
235+
if (jsonRequest.params[0].isNum()) {
236+
MilliSleep(jsonRequest.params[0].get_int());
237+
}
232238
return "Bitcoin server stopping";
233239
}
234240

@@ -255,7 +261,7 @@ static const CRPCCommand vRPCCommands[] =
255261
// --------------------- ------------------------ ----------------------- ----------
256262
/* Overall control/query calls */
257263
{ "control", "help", &help, {"command"} },
258-
{ "control", "stop", &stop, {} },
264+
{ "control", "stop", &stop, {"wait"} },
259265
{ "control", "uptime", &uptime, {} },
260266
};
261267
// clang-format on

test/functional/feature_shutdown.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2018 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test bitcoind shutdown."""
6+
7+
from test_framework.test_framework import BitcoinTestFramework
8+
from test_framework.util import assert_equal, get_rpc_proxy
9+
from threading import Thread
10+
11+
def test_long_call(node):
12+
block = node.waitfornewblock()
13+
assert_equal(block['height'], 0)
14+
15+
class ShutdownTest(BitcoinTestFramework):
16+
17+
def set_test_params(self):
18+
self.setup_clean_chain = True
19+
self.num_nodes = 1
20+
21+
def run_test(self):
22+
node = get_rpc_proxy(self.nodes[0].url, 1, timeout=600, coveragedir=self.nodes[0].coverage_dir)
23+
Thread(target=test_long_call, args=(node,)).start()
24+
# wait 1 second to ensure event loop waits for current connections to close
25+
self.stop_node(0, wait=1000)
26+
27+
if __name__ == '__main__':
28+
ShutdownTest().main()

test/functional/test_framework/test_framework.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -327,16 +327,16 @@ def start_nodes(self, extra_args=None, *args, **kwargs):
327327
for node in self.nodes:
328328
coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc)
329329

330-
def stop_node(self, i, expected_stderr=''):
330+
def stop_node(self, i, expected_stderr='', wait=0):
331331
"""Stop a bitcoind test node"""
332-
self.nodes[i].stop_node(expected_stderr)
332+
self.nodes[i].stop_node(expected_stderr, wait=wait)
333333
self.nodes[i].wait_until_stopped()
334334

335-
def stop_nodes(self):
335+
def stop_nodes(self, wait=0):
336336
"""Stop multiple bitcoind test nodes"""
337337
for node in self.nodes:
338338
# Issue RPC to stop nodes
339-
node.stop_node()
339+
node.stop_node(wait=wait)
340340

341341
for node in self.nodes:
342342
# Wait for nodes to stop

test/functional/test_framework/test_node.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,13 @@ def get_wallet_rpc(self, wallet_name):
228228
wallet_path = "wallet/{}".format(urllib.parse.quote(wallet_name))
229229
return self.rpc / wallet_path
230230

231-
def stop_node(self, expected_stderr=''):
231+
def stop_node(self, expected_stderr='', wait=0):
232232
"""Stop the node."""
233233
if not self.running:
234234
return
235235
self.log.debug("Stopping node")
236236
try:
237-
self.stop()
237+
self.stop(wait=wait)
238238
except http.client.CannotSendRequest:
239239
self.log.exception("Unable to stop node.")
240240

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
'feature_config_args.py',
187187
'rpc_help.py',
188188
'feature_help.py',
189+
'feature_shutdown.py',
189190
# Don't append tests at the end to avoid merge conflicts
190191
# Put them in a random line within the section that fits their approximate run-time
191192
]

0 commit comments

Comments
 (0)