-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Replace libevent with our own HTTP and socket-handling implementation #32061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32061. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. LLM Linter (✨ experimental)Possible typos and grammar issues:
drahtbot_id_4_m |
🚧 At least one of the CI tasks failed. HintsTry to run the tests locally, according to the documentation. However, a CI failure may still
Leave a comment here, if you need help tracking down a confusing failure. |
Concept ACK, nice work |
Concept ACK |
Concept ACK My understanding from the low-level networking discussion at CoreDev was that this wouldn't build on top of sockman. I guess the devil is in the details but can you address that in what sense the current approach follows what was discussed there? Thanks! |
Sure, by coredev I had already written most of this implementation (based on sockman) but the performance was bad, and that was part of the motivation behind the deep-dive talk. However, by the end of the week I had reviewed that code in person with smart attendees and not only improved the performance of my code but started to improve performance vs master branch as well! Those updates came in the days just after the deep-dive discussion. SOME kind of sockman is needed to replace libevent. The one @vasild wrote does actually seem to work well for this purpose as well as for p2p, and it would be "nice" to only have to maintain one I/O loop structure in bitcoind. @theuni is investigating how a sockman for http could be optimized if it had no other purpose, and I think that is the kind of feedback that will help us decide which path to take. |
640f38f
to
49784b3
Compare
💯 |
https://httpwg.org/specs/rfc9110.html#rfc.section.5.1 Field names in HTTP headers are case-insensitive. These structs will be used in the headers map to search by key. In libevent these are compared in lowercase: evhttp_find_header() evutil_ascii_strcasecmp() EVUTIL_TOLOWER_()
HTTP 1.1 responses require a timestamp header with a specific format, specified in: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
This is a helper struct to parse HTTP messages from data in buffers from sockets. HTTP messages begin with headers which are CRLF-terminated lines (\n or \r\n) followed by an arbitrary amount of body data. Whitespace is trimmed from the field lines but not the body. https://httpwg.org/specs/rfc9110.html#rfc.section.5
This commit is a no-op to isolate HTTP methods and objects that depend on libevent. Following commits will add replacement objects and methods in a new namespace for testing and review before switching over the server.
HTTP Response message: https://datatracker.ietf.org/doc/html/rfc1945#section-6 Status line (first line of response): https://datatracker.ietf.org/doc/html/rfc1945#section-6.1 Status code definitions: https://datatracker.ietf.org/doc/html/rfc1945#section-9
HTTP Request message: https://datatracker.ietf.org/doc/html/rfc1945#section-5 Request Line aka Control Line aka first line: https://datatracker.ietf.org/doc/html/rfc1945#section-5.1 See message_read_status() in libevent http.c for how `MORE_DATA_EXPECTED` is handled there
See https://www.rfc-editor.org/rfc/rfc7230#section-6.3.2 > A server MAY process a sequence of pipelined requests in parallel if they all have safe methods (Section 4.2.1 of [RFC7231]), but it MUST send the corresponding responses in the same order that the requests were received. We choose NOT to process requests in parallel. They are executed in the order recevied as well as responded to in the order received. This prevents race conditions where old state may get sent in response to requests that are very quick to process but were requested later on in the queue.
This is a refactor to prepare for matching the API of HTTPRequest definitions in both namespaces http_bitcoin and http_libevent. In particular, to provide a consistent return type for GetRequestMethod() in both classes.
These methods are called by http_request_cb() and are present in the original http_libevent::HTTPRequest.
The original function is passed to libevent as a callback when HTTP requests are received and processed. It wrapped the libevent request object in a http_libevent::HTTPRequest and then handed that off to bitcoin for basic checks and finally dispatch to worker threads. In this commit we split the function after the http_libevent::HTTPRequest is created, and pass that object to a new function that maintains the logic of checking and dispatching. This will be the merge point for http_libevent and http_bitcoin, where HTTPRequest objects from either namespace have the same downstream lifecycle.
The original function was already naturally split into two chunks: First, we parse and validate the users' RPC configuration for IPs and ports. Next we bind libevent's http server to the appropriate endpoints. This commit splits these chunks into two separate functions, leaving the argument parsing in the common space of the module and moving the libevent-specific binding into the http_libevent namespace. A future commit will implement http_bitcoin::HTTPBindAddresses to bind the validate list of endpoints by the new HTTP server.
aa89c97
to
cdb13c9
Compare
This removes the dependency on libevent for scheduled events, like re-locking a wallet some time after decryption.
cdb13c9
to
e531a7c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test/functional/interface_http.py
Outdated
conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) | ||
conn.connect() | ||
sock = conn.sock | ||
sock.settimeout(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #32408
test/functional/interface_http.py
Outdated
# The server should not respond to the fast, second request | ||
# until the (very) slow first request has been handled: | ||
res = sock.recv(1024) | ||
assert not res |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #32408
test/functional/interface_http.py
Outdated
body_chunked = [ | ||
b'{"method": "submitblock", "params": ["', | ||
b'0A' * 1000000, | ||
b'0B' * 1000000, | ||
b'0C' * 1000000, | ||
b'0D' * 1000000, | ||
b'"]}' | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #32408
test/functional/interface_http.py
Outdated
headers=headers_chunked, | ||
encode_chunked=True) | ||
out1 = conn.getresponse().read() | ||
assert out1 == b'{"result":"high-hash","error":null}\n' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #32408
test/functional/interface_http.py
Outdated
# definitely closed | ||
try: | ||
conn.request('GET', '/') | ||
conn.getresponse() | ||
# macos/linux windows | ||
except (ConnectionResetError, ConnectionAbortedError): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #32408
src/httpserver.cpp
Outdated
void HTTPServer::CloseConnectionInternal(std::shared_ptr<HTTPClient>& client) | ||
{ | ||
event_free(ev); | ||
if (CloseConnection(client->m_node_id)) { | ||
LogDebug(BCLog::HTTP, "Disconnected HTTP client %s (id=%d)\n", client->m_origin, client->m_node_id); | ||
} else { | ||
LogDebug(BCLog::HTTP, "Failed to disconnect non-existent HTTP client %s (id=%d)\n", client->m_origin, client->m_node_id); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes thanks
src/httpserver.h
Outdated
// Set to true when we receive request data and set to false once m_send_buffer is cleared. | ||
// Checked during DisconnectClients(). All of these operations take place in the Sockman I/O loop, | ||
// however it may get set my a worker thread during an "optimistic send". | ||
std::atomic_bool m_prevent_disconnect{false}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/httpserver.h
Outdated
std::string Stringify() const; | ||
|
||
private: | ||
std::map<std::string, std::string, util::CaseInsensitiveComparator> m_map; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great feedback thanks. I made this change by first editing the "string: add CaseInsensitiveComparator"
commit which is now "string: add CaseInsensitive{KeyEqual, Hash} for unordered map"
and implements the two operators needed for the unordered map.
src/httpserver.cpp
Outdated
std::optional<std::string_view> HTTPHeaders::Find(const std::string key) const | ||
{ | ||
if (!InitHTTPAllowList()) | ||
return false; | ||
const auto it = m_map.find(key); | ||
if (it == m_map.end()) return std::nullopt; | ||
return std::string_view(it->second); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah that is another great catch thank you. Glad I wrote that as an extra commit! I'll pop it off, it was added in response to this review comment: #32061 (comment)
src/httpserver.cpp
Outdated
auto content_length_value{m_headers.Find("Content-Length")}; | ||
if (!content_length_value) return true; | ||
|
||
uint64_t content_length; | ||
if (!ParseUInt64(content_length_value.value(), &content_length)) throw std::runtime_error("Cannot parse Content-Length value"); | ||
|
||
// Not enough data in buffer for expected body | ||
if (reader.Left() < content_length) return false; | ||
|
||
m_body = reader.ReadLength(content_length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah its true, a client could send a request with a huge list of HTTP headers and an incomplete body and we would parse the headers over and over again from m_recv_buffer
until the body was finally completed, when we finally pinch off a HTTPRequest
and erase the data from the buffer.
There certainly is an optimization there, keeping an in-progress request attached to HTTPClient
with a "where we left off" pointer. It sounds like you would be ok with seeing that in a follow-up PR but let me know if its blocking for you here.
This is a major component of removing libevent as a dependency of the project
It is based on #30988 but only in the sense that it copies the
Sockman
class introduced in that PR. The p2p /Connman
refactor isn't needed for HTTP and therefore this branch can be reviewed and merged independently of the p2p changes.Commit strategy:
sockman.{h,cpp}
from Split CConnman #30988http_libevent
http_bitcoin
(the namespace manages duplicateHTTPRequest
definitions, etc)http_libevent
)I am currently seeing about a 10% speed up in the functional tests on my own arm/macos machine.
Integration testing:
I am testing the new HTTP server by forking projects that integrate with bitcoin via HTTP and running their integration tests with bitcoind built from this branch (on Github actions). I will continue adding integrations over time, and re-running these CI tests as this branch gets rebased:
rpc-bitcoin