-
Notifications
You must be signed in to change notification settings - Fork 53
proxy: add local connection limit to ListenConnections #269
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,8 +7,18 @@ Library versions are tracked with simple | |
| Versioning policy is described in the [version.h](../include/mp/version.h) | ||
| include. | ||
|
|
||
| ## v11 | ||
| ## v12 | ||
| - Current unstable version. | ||
| - Adds an optional per-listener `max_connections` parameter to `ListenConnections()` | ||
| so servers can stop accepting new connections when a local connection cap is reached, | ||
| and resume accepting after existing connections disconnect. | ||
|
|
||
| ## [v11.0](https://github.com/bitcoin-core/libmultiprocess/commits/v11.0) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "doc/version: Bump version 11 > 12" (2f7f1bf) These updates look right but might better to use the more complete list of changes from bitcoin/bitcoin#35661. Suggested diff is below, but also feel free to keep current test or just replace the list with a TBD comment. diff
--- a/doc/versions.md
+++ b/doc/versions.md
@@ -14,11 +14,17 @@ include.
and resume accepting after existing connections disconnect.
## [v11.0](https://github.com/bitcoin-core/libmultiprocess/commits/v11.0)
-- Tolerates unexpected exceptions in event loop `post()` callbacks.
-- Tolerates exceptions from remote destroy during cleanup in `ProxyClient`.
-- Supports primitive `std::optional` struct fields in the code generator (`mpgen`).
-- Adds `TypeName()` and improves debug log coverage for Proxy object lifecycle.
-- Updates build compatibility with recent Nix and CMake versions.
+- Adds `makePool` method on `ThreadMap` to support thread pool routing, allowing requests without a specific client thread to be dispatched to a pool using a shortest-queue strategy ([#283](https://github.com/bitcoin-core/libmultiprocess/pull/283)).
+- Adds `std::unordered_set` support, a `BuildList` helper, and a `ReadList` helper to reduce duplication in list build and read handlers ([#277](https://github.com/bitcoin-core/libmultiprocess/pull/277), [#285](https://github.com/bitcoin-core/libmultiprocess/pull/285)).
+- Adds support for translating C++ `std::optional<T>` struct fields to pairs of `T` + `hasT :Bool` Cap'n Proto struct fields, allowing unset optional primitive fields to be represented ([#243](https://github.com/bitcoin-core/libmultiprocess/pull/243)).
+- Produces more readable log output for Proxy object lifecycle events and IPC server-side failures ([#218](https://github.com/bitcoin-core/libmultiprocess/pull/218)).
+- Handles exceptions thrown by `destroy` methods by logging instead of aborting ([#273](https://github.com/bitcoin-core/libmultiprocess/pull/273)). This can prevent server crashes when non-libmultiprocess clients disconnect without destroying objects, in the case where a server object owns client objects and the server destructor tries to call the disconnected client to free them ([#219](https://github.com/bitcoin-core/libmultiprocess/issues/219)).
+- Handles unexpected exceptions thrown by callbacks (that should never happen) by logging errors instead of deadlocking ([#260](https://github.com/bitcoin-core/libmultiprocess/pull/260)).
+- Fixes a rare mptest hang on musl builds caused by a lost wakeup bug in `Waiter` ([#295](https://github.com/bitcoin-core/libmultiprocess/pull/295)).
+- Fixes a race condition in a log print detected by TSan ([#286](https://github.com/bitcoin-core/libmultiprocess/pull/286)).
+- Build improvements: makes `target_capnp_sources` work correctly when libmultiprocess is used as a CMake subproject ([#289](https://github.com/bitcoin-core/libmultiprocess/pull/289)), adds `mp_headers` target for better lint tool support ([#291](https://github.com/bitcoin-core/libmultiprocess/pull/291)), and fixes compatibility with recent Nix and CMake 4.0 ([#238](https://github.com/bitcoin-core/libmultiprocess/pull/238)).
+- Test, CI, documentation, and minor code improvements: design document corrections ([#278](https://github.com/bitcoin-core/libmultiprocess/pull/278)), field constant comments ([#279](https://github.com/bitcoin-core/libmultiprocess/pull/279)), clang-tidy fix ([#292](https://github.com/bitcoin-core/libmultiprocess/pull/292)), new smoke test for double-precision float values ([#294](https://github.com/bitcoin-core/libmultiprocess/pull/294)), new test for recursive async IPC calls ([#301](https://github.com/bitcoin-core/libmultiprocess/pull/301)), removal of libevent from Core CI builds ([#299](https://github.com/bitcoin-core/libmultiprocess/pull/299)), and rename of `EventLoop::m_num_clients` to `m_num_refs` ([#302](https://github.com/bitcoin-core/libmultiprocess/pull/302)).
+- Used in Bitcoin Core master branch, pulled in by [#35661](https://github.com/bitcoin/bitcoin/pull/35661).
## [v10.0](https://github.com/bitcoin-core/libmultiprocess/commits/v10.0)
- Increases spawn test timeout to avoid spurious failures. |
||
| - Tolerates unexpected exceptions in event loop `post()` callbacks. | ||
| - Tolerates exceptions from remote destroy during cleanup in `ProxyClient`. | ||
| - Supports primitive `std::optional` struct fields in the code generator (`mpgen`). | ||
| - Adds `TypeName()` and improves debug log coverage for Proxy object lifecycle. | ||
| - Updates build compatibility with recent Nix and CMake versions. | ||
|
|
||
| ## [v10.0](https://github.com/bitcoin-core/libmultiprocess/commits/v10.0) | ||
| - Increases spawn test timeout to avoid spurious failures. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,9 @@ | |
| #include <capnp/rpc-twoparty.h> | ||
|
|
||
| #include <assert.h> | ||
| #include <algorithm> | ||
| #include <condition_variable> | ||
| #include <cstdlib> | ||
| #include <functional> | ||
| #include <kj/function.h> | ||
| #include <map> | ||
|
|
@@ -25,6 +27,7 @@ | |
|
|
||
| namespace mp { | ||
| struct ThreadContext; | ||
| struct Listener; | ||
|
|
||
| struct InvokeContext | ||
| { | ||
|
|
@@ -356,6 +359,12 @@ class EventLoop | |
|
|
||
| //! Hook called on the worker thread just before returning results. | ||
| std::function<void()> testing_hook_async_request_done; | ||
|
|
||
| //! Hook called on the server thread when the client has connected. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "test: add dedicated ListenConnections coverage" (ef35679) Would change "server thread" to "event loop thread". Saying server thread is a little confusing because typically there's only one event loop and it processes all events on a single thread, not distinguishing between client and server events. Would also change "the client" to "a client" since there may be more than one. Same suggestion also applies to next commit adding disconnect hook. |
||
| std::function<void()> testing_hook_connected; | ||
|
|
||
| //! Hook called on the server thread when the client has disconnected. | ||
| std::function<void()> testing_hook_disconnected; | ||
| }; | ||
|
|
||
| //! Single element task queue used to handle recursive capnp calls. (If the | ||
|
|
@@ -831,8 +840,8 @@ std::unique_ptr<ProxyClient<InitInterface>> ConnectStream(EventLoop& loop, int f | |
| //! handles requests from the stream by calling the init object. Embed the | ||
| //! ProxyServer in a Connection object that is stored and erased if | ||
| //! disconnected. This should be called from the event loop thread. | ||
| template <typename InitInterface, typename InitImpl> | ||
| void _Serve(EventLoop& loop, kj::Own<kj::AsyncIoStream>&& stream, InitImpl& init) | ||
| template <typename InitInterface, typename InitImpl, typename OnDisconnect> | ||
| void _Serve(EventLoop& loop, kj::Own<kj::AsyncIoStream>&& stream, InitImpl& init, OnDisconnect&& on_disconnect) | ||
| { | ||
| loop.m_incoming_connections.emplace_front(loop, kj::mv(stream), [&](Connection& connection) { | ||
| // Disable deleter so proxy server object doesn't attempt to delete the | ||
|
|
@@ -842,23 +851,51 @@ void _Serve(EventLoop& loop, kj::Own<kj::AsyncIoStream>&& stream, InitImpl& init | |
| }); | ||
| auto it = loop.m_incoming_connections.begin(); | ||
| MP_LOG(loop, Log::Info) << "IPC server: socket connected."; | ||
| it->onDisconnect([&loop, it] { | ||
| if (loop.testing_hook_connected) loop.testing_hook_connected(); | ||
| it->onDisconnect([&loop, it, on_disconnect = std::forward<OnDisconnect>(on_disconnect)]() mutable { | ||
| MP_LOG(loop, Log::Info) << "IPC server: socket disconnected."; | ||
| loop.m_incoming_connections.erase(it); | ||
| on_disconnect(); | ||
| if (loop.testing_hook_disconnected) loop.testing_hook_disconnected(); | ||
| }); | ||
| } | ||
|
|
||
| //! Given connection receiver and an init object, handle incoming connections by | ||
| //! calling _Serve, to create ProxyServer objects and forward requests to the | ||
| //! init object. | ||
| struct Listener | ||
| { | ||
| explicit Listener(kj::Own<kj::ConnectionReceiver>&& receiver, std::optional<size_t> max_connections) | ||
| : m_receiver(kj::mv(receiver)), m_max_connections(max_connections) {} | ||
|
|
||
| bool atCapacity() const | ||
| { | ||
| return m_max_connections && m_active_connections >= *m_max_connections; | ||
| } | ||
|
|
||
| //! Handle incoming connections by calling _Serve, to create ProxyServer | ||
| //! objects and forward requests to the init object. | ||
| template <typename InitInterface, typename InitImpl> | ||
| void listen(EventLoop& loop, InitImpl& init, const std::shared_ptr<Listener>& self); | ||
|
|
||
| kj::Own<kj::ConnectionReceiver> m_receiver; | ||
| std::optional<size_t> m_max_connections; | ||
| size_t m_active_connections{0}; | ||
| }; | ||
|
|
||
| template <typename InitInterface, typename InitImpl> | ||
| void _Listen(EventLoop& loop, kj::Own<kj::ConnectionReceiver>&& listener, InitImpl& init) | ||
| void Listener::listen(EventLoop& loop, InitImpl& init, const std::shared_ptr<Listener>& self) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "test: add dedicated ListenConnections coverage" (ef35679) I think it's a little confusing and creates an opportunity for bugs that a non-static method is taking a self parameter, and could potentially be called with two different Also, if we want to start returning For both of these reasons would suggest dropping the Suggested change: diff
--- a/include/mp/proxy-io.h
+++ b/include/mp/proxy-io.h
@@ -870,32 +870,27 @@ struct Listener
return m_max_connections && m_active_connections >= *m_max_connections;
}
- //! Handle incoming connections by calling _Serve, to create ProxyServer
- //! objects and forward requests to the init object.
- template <typename InitInterface, typename InitImpl>
- void listen(EventLoop& loop, InitImpl& init, const std::shared_ptr<Listener>& self);
-
kj::Own<kj::ConnectionReceiver> m_receiver;
std::optional<size_t> m_max_connections;
size_t m_active_connections{0};
};
template <typename InitInterface, typename InitImpl>
-void Listener::listen(EventLoop& loop, InitImpl& init, const std::shared_ptr<Listener>& self)
+void _Listen(const std::shared_ptr<Listener>& listener, EventLoop& loop, InitImpl& init)
{
- if (atCapacity()) return;
+ if (listener->atCapacity()) return;
- auto* receiver = m_receiver.get();
+ auto* receiver = listener->m_receiver.get();
loop.m_task_set->add(receiver->accept().then(
- [&loop, &init, self](kj::Own<kj::AsyncIoStream>&& stream) {
- ++self->m_active_connections;
- _Serve<InitInterface>(loop, kj::mv(stream), init, [&loop, &init, self] {
- const bool resume_accept{self->atCapacity()};
- assert(self->m_active_connections > 0);
- --self->m_active_connections;
- if (resume_accept) self->listen<InitInterface>(loop, init, self);
+ [&loop, &init, listener](kj::Own<kj::AsyncIoStream>&& stream) {
+ ++listener->m_active_connections;
+ _Serve<InitInterface>(loop, kj::mv(stream), init, [&loop, &init, listener] {
+ const bool resume_accept{listener->atCapacity()};
+ assert(listener->m_active_connections > 0);
+ --listener->m_active_connections;
+ if (resume_accept) _Listen<InitInterface>(listener, loop, init);
});
- self->listen<InitInterface>(loop, init, self);
+ _Listen<InitInterface>(listener, loop, init);
}));
}
@@ -920,7 +915,7 @@ void ListenConnections(EventLoop& loop, int fd, InitImpl& init, std::optional<si
auto listener{std::make_shared<Listener>(
loop.m_io_context.lowLevelProvider->wrapListenSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP),
max_connections)};
- listener->listen<InitInterface>(loop, init, listener);
+ _Listen<InitInterface>(listener, loop, init);
});
}
|
||
| { | ||
| auto* ptr = listener.get(); | ||
| loop.m_task_set->add(ptr->accept().then( | ||
| [&loop, &init, listener = kj::mv(listener)](kj::Own<kj::AsyncIoStream>&& stream) mutable { | ||
| _Serve<InitInterface>(loop, kj::mv(stream), init); | ||
| _Listen<InitInterface>(loop, kj::mv(listener), init); | ||
| if (atCapacity()) return; | ||
|
|
||
| auto* receiver = m_receiver.get(); | ||
| loop.m_task_set->add(receiver->accept().then( | ||
| [&loop, &init, self](kj::Own<kj::AsyncIoStream>&& stream) { | ||
| ++self->m_active_connections; | ||
| _Serve<InitInterface>(loop, kj::mv(stream), init, [&loop, &init, self] { | ||
| const bool resume_accept{self->atCapacity()}; | ||
| assert(self->m_active_connections > 0); | ||
| --self->m_active_connections; | ||
| if (resume_accept) self->listen<InitInterface>(loop, init, self); | ||
| }); | ||
| self->listen<InitInterface>(loop, init, self); | ||
| })); | ||
| } | ||
|
|
||
|
|
@@ -868,18 +905,22 @@ template <typename InitInterface, typename InitImpl> | |
| void ServeStream(EventLoop& loop, int fd, InitImpl& init) | ||
| { | ||
| _Serve<InitInterface>( | ||
| loop, loop.m_io_context.lowLevelProvider->wrapSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP), init); | ||
| loop, | ||
| loop.m_io_context.lowLevelProvider->wrapSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP), | ||
| init, | ||
| [] {}); | ||
| } | ||
|
|
||
| //! Given listening socket file descriptor and an init object, handle incoming | ||
| //! connections and requests by calling methods on the Init object. | ||
| template <typename InitInterface, typename InitImpl> | ||
| void ListenConnections(EventLoop& loop, int fd, InitImpl& init) | ||
| void ListenConnections(EventLoop& loop, int fd, InitImpl& init, std::optional<size_t> max_connections = std::nullopt) | ||
| { | ||
| loop.sync([&]() { | ||
| _Listen<InitInterface>(loop, | ||
| auto listener{std::make_shared<Listener>( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "proxy: add local connection limit to ListenConnections" (db81e9c) Just want to note for followup that it will probably make sense to return this listener shared_ptr to the caller to allow it to stop listening. Right now there isn't (and has never been) a way to stop listening without shutting down the entire event loop, but this could now be supported with the listener object. (see also #269 (comment))
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having a way to make a stop a listener from listening is something that should definitiely be supported. And returning the |
||
| loop.m_io_context.lowLevelProvider->wrapListenSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP), | ||
| init); | ||
| max_connections)}; | ||
| listener->listen<InitInterface>(loop, init, listener); | ||
| }); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.