Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _libmultiprocess_ is a library and code generator that allows calling C++ class

The `*.capnp` data definition files are consumed by the _libmultiprocess_ code generator and each `X.capnp` file generates `X.capnp.c++`, `X.capnp.h`, `X.capnp.proxy-client.c++`, `X.capnp.proxy-server.c++`, `X.capnp.proxy-types.c++`, `X.capnp.proxy-types.h`, and `X.capnp.proxy.h` output files. The generated files include `mp::ProxyClient<Interface>` and `mp::ProxyServer<Interface>` class specializations for all the interfaces in the `.capnp` files. These allow methods on C++ objects in one process to be called from other processes over IPC sockets.

The `ProxyServer` objects help translate IPC requests from a socket to method calls on a local object. The `ProxyServer` objects are just used internally by the `mp::ServeStream(loop, socket, wrapped_object)` and `mp::ListenConnections(loop, socket, wrapped_object)` functions, and aren't exposed externally. The `ProxyClient` classes are exposed, and returned from the `mp::ConnectStream(loop, socket)` function and meant to be used directly. The classes implement methods described in `.capnp` definitions, and whenever any method is called, a request with the method arguments is sent over the associated IPC connection, and the corresponding `wrapped_object` method on the other end of the connection is called, with the `ProxyClient` method blocking until it returns and forwarding back any return value to the `ProxyClient` method caller.
The `ProxyServer` objects help translate IPC requests from a socket to method calls on a local object. The `ProxyServer` objects are just used internally by the `mp::ServeStream(loop, socket, wrapped_object)` and `mp::ListenConnections(loop, socket, wrapped_object[, max_connections])` functions, and aren't exposed externally. The `ProxyClient` classes are exposed, and returned from the `mp::ConnectStream(loop, socket)` function and meant to be used directly. The classes implement methods described in `.capnp` definitions, and whenever any method is called, a request with the method arguments is sent over the associated IPC connection, and the corresponding `wrapped_object` method on the other end of the connection is called, with the `ProxyClient` method blocking until it returns and forwarding back any return value to the `ProxyClient` method caller.

## Example

Expand Down
12 changes: 11 additions & 1 deletion doc/versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
ryanofsky marked this conversation as resolved.
- 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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
Expand Down
73 changes: 57 additions & 16 deletions include/mp/proxy-io.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -25,6 +27,7 @@

namespace mp {
struct ThreadContext;
struct Listener;

struct InvokeContext
{
Expand Down Expand Up @@ -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.

@ryanofsky ryanofsky Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 Listener instances.

Also, if we want to start returning Listener objects from ListenConnections to applications to stop listening, it would be confusing for this method to be part of the public interface, since applications should never call it.

For both of these reasons would suggest dropping the Listen::listener method, and just keeping the previous _Listen function which is only meant to be used internally and not called by applications. This would also be a code simplification

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);
}));
}

Expand All @@ -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>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 std::shared_ptr<Listener> later as a way to potentially do that seems like a good starting point.

loop.m_io_context.lowLevelProvider->wrapListenSocketFd(fd, kj::LowLevelAsyncIoProvider::TAKE_OWNERSHIP),
init);
max_connections)};
listener->listen<InitInterface>(loop, init, listener);
});
}

Expand Down
2 changes: 1 addition & 1 deletion include/mp/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
//! pointing at the prior merge commit. The /doc/versions.md file should also be
//! updated, noting any significant or incompatible changes made since the
//! previous version.
#define MP_MAJOR_VERSION 11
#define MP_MAJOR_VERSION 12

//! Minor version number. Should be incremented in stable branches after
//! backporting changes. The /doc/versions.md file should also be updated to
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ if(BUILD_TESTING AND TARGET CapnProto::kj-test)
${MP_PROXY_HDRS}
mp/test/foo-types.h
mp/test/foo.h
mp/test/listen_tests.cpp
mp/test/spawn_tests.cpp
mp/test/test.cpp
)
Expand Down
Loading
Loading