Merge bitcoin/bitcoin#24356: refactor: replace CConnman::SocketEvents() with mockable Sock::WaitMany()

6e68ccbefe net: use Sock::WaitMany() instead of CConnman::SocketEvents() (Vasil Dimov)
ae263460ba net: introduce Sock::WaitMany() (Vasil Dimov)
cc74459768 net: also wait for exceptional events in Sock::Wait() (Vasil Dimov)

Pull request description:

  _This is a piece of #21878, chopped off to ease review._

  `Sock::Wait()` waits for IO events on one socket. Introduce a similar `virtual` method `WaitMany()` that waits simultaneously for IO events on more than one socket.

  Use `WaitMany()` instead of `CConnman::SocketEvents()` (and ditch the latter). Given that the former is a `virtual` method, it can be mocked by unit and fuzz tests. This will help to make bigger parts of `CConnman` testable (unit and fuzz).

ACKs for top commit:
  laanwj:
    Code review ACK 6e68ccbefe
  jonatack:
    re-ACK 6e68ccbefe per `git range-diff e18fd47 6747729 6e68ccb`, and verified rebase to master and debug build

Tree-SHA512: 917fb6ad880d64d3af1ebb301c06fbd01afd8ff043f49e4055a088ebed6affb7ffe1dcf59292d822f10de5f323b6d52d557cb081dd7434634995f9148efcf08f
This commit is contained in:
laanwj
2022-06-16 19:55:03 +02:00
8 changed files with 208 additions and 233 deletions

View File

@@ -113,63 +113,103 @@ int Sock::SetSockOpt(int level, int opt_name, const void* opt_val, socklen_t opt
bool Sock::Wait(std::chrono::milliseconds timeout, Event requested, Event* occurred) const
{
#ifdef USE_POLL
pollfd fd;
fd.fd = m_socket;
fd.events = 0;
if (requested & RECV) {
fd.events |= POLLIN;
}
if (requested & SEND) {
fd.events |= POLLOUT;
}
// We need a `shared_ptr` owning `this` for `WaitMany()`, but don't want
// `this` to be destroyed when the `shared_ptr` goes out of scope at the
// end of this function. Create it with a custom noop deleter.
std::shared_ptr<const Sock> shared{this, [](const Sock*) {}};
if (poll(&fd, 1, count_milliseconds(timeout)) == SOCKET_ERROR) {
EventsPerSock events_per_sock{std::make_pair(shared, Events{requested})};
if (!WaitMany(timeout, events_per_sock)) {
return false;
}
if (occurred != nullptr) {
*occurred = 0;
if (fd.revents & POLLIN) {
*occurred |= RECV;
*occurred = events_per_sock.begin()->second.occurred;
}
return true;
}
bool Sock::WaitMany(std::chrono::milliseconds timeout, EventsPerSock& events_per_sock) const
{
#ifdef USE_POLL
std::vector<pollfd> pfds;
for (const auto& [sock, events] : events_per_sock) {
pfds.emplace_back();
auto& pfd = pfds.back();
pfd.fd = sock->m_socket;
if (events.requested & RECV) {
pfd.events |= POLLIN;
}
if (fd.revents & POLLOUT) {
*occurred |= SEND;
if (events.requested & SEND) {
pfd.events |= POLLOUT;
}
}
if (poll(pfds.data(), pfds.size(), count_milliseconds(timeout)) == SOCKET_ERROR) {
return false;
}
assert(pfds.size() == events_per_sock.size());
size_t i{0};
for (auto& [sock, events] : events_per_sock) {
assert(sock->m_socket == static_cast<SOCKET>(pfds[i].fd));
events.occurred = 0;
if (pfds[i].revents & POLLIN) {
events.occurred |= RECV;
}
if (pfds[i].revents & POLLOUT) {
events.occurred |= SEND;
}
if (pfds[i].revents & (POLLERR | POLLHUP)) {
events.occurred |= ERR;
}
++i;
}
return true;
#else
if (!IsSelectableSocket(m_socket)) {
return false;
}
fd_set recv;
fd_set send;
fd_set err;
FD_ZERO(&recv);
FD_ZERO(&send);
FD_ZERO(&err);
SOCKET socket_max{0};
fd_set fdset_recv;
fd_set fdset_send;
FD_ZERO(&fdset_recv);
FD_ZERO(&fdset_send);
if (requested & RECV) {
FD_SET(m_socket, &fdset_recv);
}
if (requested & SEND) {
FD_SET(m_socket, &fdset_send);
}
timeval timeout_struct = MillisToTimeval(timeout);
if (select(m_socket + 1, &fdset_recv, &fdset_send, nullptr, &timeout_struct) == SOCKET_ERROR) {
return false;
}
if (occurred != nullptr) {
*occurred = 0;
if (FD_ISSET(m_socket, &fdset_recv)) {
*occurred |= RECV;
for (const auto& [sock, events] : events_per_sock) {
const auto& s = sock->m_socket;
if (!IsSelectableSocket(s)) {
return false;
}
if (FD_ISSET(m_socket, &fdset_send)) {
*occurred |= SEND;
if (events.requested & RECV) {
FD_SET(s, &recv);
}
if (events.requested & SEND) {
FD_SET(s, &send);
}
FD_SET(s, &err);
socket_max = std::max(socket_max, s);
}
timeval tv = MillisToTimeval(timeout);
if (select(socket_max + 1, &recv, &send, &err, &tv) == SOCKET_ERROR) {
return false;
}
for (auto& [sock, events] : events_per_sock) {
const auto& s = sock->m_socket;
events.occurred = 0;
if (FD_ISSET(s, &recv)) {
events.occurred |= RECV;
}
if (FD_ISSET(s, &send)) {
events.occurred |= SEND;
}
if (FD_ISSET(s, &err)) {
events.occurred |= ERR;
}
}

View File

@@ -12,6 +12,7 @@
#include <chrono>
#include <memory>
#include <string>
#include <unordered_map>
/**
* Maximum time to wait for I/O readiness.
@@ -130,26 +131,84 @@ public:
/**
* If passed to `Wait()`, then it will wait for readiness to read from the socket.
*/
static constexpr Event RECV = 0b01;
static constexpr Event RECV = 0b001;
/**
* If passed to `Wait()`, then it will wait for readiness to send to the socket.
*/
static constexpr Event SEND = 0b10;
static constexpr Event SEND = 0b010;
/**
* Ignored if passed to `Wait()`, but could be set in the occurred events if an
* exceptional condition has occurred on the socket or if it has been disconnected.
*/
static constexpr Event ERR = 0b100;
/**
* Wait for readiness for input (recv) or output (send).
* @param[in] timeout Wait this much for at least one of the requested events to occur.
* @param[in] requested Wait for those events, bitwise-or of `RECV` and `SEND`.
* @param[out] occurred If not nullptr and `true` is returned, then upon return this
* indicates which of the requested events occurred. A timeout is indicated by return
* value of `true` and `occurred` being set to 0.
* @return true on success and false otherwise
* @param[out] occurred If not nullptr and the function returns `true`, then this
* indicates which of the requested events occurred (`ERR` will be added, even if
* not requested, if an exceptional event occurs on the socket).
* A timeout is indicated by return value of `true` and `occurred` being set to 0.
* @return true on success (or timeout, if `occurred` of 0 is returned), false otherwise
*/
[[nodiscard]] virtual bool Wait(std::chrono::milliseconds timeout,
Event requested,
Event* occurred = nullptr) const;
/**
* Auxiliary requested/occurred events to wait for in `WaitMany()`.
*/
struct Events {
explicit Events(Event req) : requested{req}, occurred{0} {}
Event requested;
Event occurred;
};
struct HashSharedPtrSock {
size_t operator()(const std::shared_ptr<const Sock>& s) const
{
return s ? s->m_socket : std::numeric_limits<SOCKET>::max();
}
};
struct EqualSharedPtrSock {
bool operator()(const std::shared_ptr<const Sock>& lhs,
const std::shared_ptr<const Sock>& rhs) const
{
if (lhs && rhs) {
return lhs->m_socket == rhs->m_socket;
}
if (!lhs && !rhs) {
return true;
}
return false;
}
};
/**
* On which socket to wait for what events in `WaitMany()`.
* The `shared_ptr` is copied into the map to ensure that the `Sock` object
* is not destroyed (its destructor would close the underlying socket).
* If this happens shortly before or after we call `poll(2)` and a new
* socket gets created under the same file descriptor number then the report
* from `WaitMany()` will be bogus.
*/
using EventsPerSock = std::unordered_map<std::shared_ptr<const Sock>, Events, HashSharedPtrSock, EqualSharedPtrSock>;
/**
* Same as `Wait()`, but wait on many sockets within the same timeout.
* @param[in] timeout Wait this long for at least one of the requested events to occur.
* @param[in,out] events_per_sock Wait for the requested events on these sockets and set
* `occurred` for the events that actually occurred.
* @return true on success (or timeout, if all `what[].occurred` are returned as 0),
* false otherwise
*/
[[nodiscard]] virtual bool WaitMany(std::chrono::milliseconds timeout,
EventsPerSock& events_per_sock) const;
/* Higher level, convenience, methods. These may throw. */
/**