Add makePool method on ThreadMap#283
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please copy-paste 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. |
makePool method on ThreadMapmakePool method on ThreadMap
d56e637 to
158f30d
Compare
makePool method on ThreadMapmakePool method on ThreadMap
8050527 to
0ac739b
Compare
|
Concept ACK. This is a good idea that should make development of rust & python clients easier, while making the behavior opt-in and not affecting c++ clients or existing clients using current behavior. With this feature, we may also want to add a way to mark certain methods that can take long time before returning to not use the thread pool, so they do not block other work. These methods could create their own threads to run on, or require threads to be specified.
Actually I think this is not an issue. If a server method is called with a callbackThread value it can still pass the same value back to the client when making the callback, no matter where the server thread is running.
Yes I need to look more closely at current implementation, but would seem better to have a single queue of waiting tasks instead of giving each thread its own queue. Otherwise execution order could be very unpredictable and if a single request is slow, it could create a backlog of requests assigned to the same thread. It seems like it should be possible to have a shared queue without too much complexity, so probably worth looking into. |
|
Concept ACK. This feature significantly reduces integration burden for clients written in other programming languages. I just glanced at it, planning to review it deeply soon. One thing to note: This PR will definitely need release notes. |
|
Would be nice to have a before and after usage example (with |
|
Should I take a hack at implementing proper work delegation throughout the pool or would reviewers prefer I keep it as is for now? |
|
re: #283 (comment)
On my side, I'd love to see a more elaborate pool. I was thinking about something like https://github.com/bitcoin/bitcoin/blob/master/src/util/threadpool.h as reference |
|
Just tested 0ac739b on peer-observer's ipc-extractor with a It's working as expected and, notably, reduces the overhead of creating a |
0ac739b to
acce408
Compare
|
Re-implemented as a "shortest queue" pool, where the thread with the lowest count of pending jobs is selected, tiebreaker by lowest index. I'd say this is more than sufficient for now (I would imagine users would not have more outstanding jobs than allocated threads in the current mining use case). |
7973f8d to
a828771
Compare
ViniciusCestarii
left a comment
There was a problem hiding this comment.
Shortest-queue selection is looking good
a828771 to
baab0fe
Compare
|
Nice feedback, updated baab0fe |
xyzconstant
left a comment
There was a problem hiding this comment.
Thanks for the changes @rustaceanrob
I exercised this test again: #283 (comment) and downstream IPC tests, everything is working as expected.
I am still reviewing but I have left a couple of comments.
Note that doc/design.md needs to be updated too to reflect this new feature
| } else { | ||
| MP_LOG(loop, Log::Error) | ||
| << "IPC server error request #" << req << ", pool thread not found"; | ||
| throw std::runtime_error("pool thread not found"); |
There was a problem hiding this comment.
I'm wondering how this block could be exercised. A test for this would be great.
There was a problem hiding this comment.
This can only happen if a worker is removed from m_threads and not m_thread_pool, which only be possible if we added a shrink/resize method. I could add something like this, but for now I'm not sure if it's necessary. FWIW the previous else path also seems untested.
diff --git a/test/mp/test/test.cpp b/test/mp/test/test.cpp
index 9be8742..cd5d5f4 100644
--- a/test/mp/test/test.cpp
+++ b/test/mp/test/test.cpp
@@ -598,5 +598,38 @@ KJ_TEST("Call async IPC method without thread or pool errors correctly")
KJ_EXPECT(error_thrown);
}
+KJ_TEST("Pool dispatch with torn-down slot surfaces error")
+{
+ TestSetup setup;
+ ProxyClient<messages::FooInterface>* foo = setup.client.get();
+ setup.server->m_impl->m_fn = [] {};
+
+ std::promise<void> done;
+ bool error_thrown{false};
+ foo->m_context.loop->sync([&] {
+ foo->m_context.connection->m_thread_pool.push_back(
+ {Thread::Client{KJ_EXCEPTION(FAILED, "pool slot torn down")}});
+
+ auto request{foo->m_client.callFnAsyncRequest()};
+ request.initContext();
+ foo->m_context.loop->m_task_set->add(
+ request.send().then(
+ [&](auto&&) { done.set_value(); },
+ [&](kj::Exception&& e) {
+ error_thrown = true;
+ KJ_EXPECT(std::string_view{e.getDescription().cStr()}.find(
+ "pool thread not found") != std::string_view::npos);
+ done.set_value();
+ }));
+ });
+ done.get_future().get();
+ KJ_EXPECT(error_thrown);
+}
+| if (pool[i].depth < slot->depth) slot = &pool[i]; | ||
| } | ||
| ++slot->depth; | ||
| auto guard = kj::defer([slot] { --slot->depth; }); |
There was a problem hiding this comment.
nit: KJ_DEFER macro would be more consistent IMO.
There was a problem hiding this comment.
KJ_DEFER doesn't support move assignment
There was a problem hiding this comment.
I actually meant something like this:
diff --git a/include/mp/type-context.h b/include/mp/type-context.h
index d241d95..e31ceee 100644
--- a/include/mp/type-context.h
+++ b/include/mp/type-context.h
@@ -223,9 +223,9 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
if (pool[i].depth < slot->depth) slot = &pool[i];
}
++slot->depth;
- auto guard = kj::defer([slot] { --slot->depth; });
return connection->m_threads.getLocalServer(slot->client)
- .then([&loop, invoke = kj::mv(invoke), req](const kj::Maybe<Thread::Server&>& pool_perhaps) mutable {
+ .then([&loop, invoke = kj::mv(invoke), req, slot](const kj::Maybe<Thread::Server&>& pool_perhaps) mutable {
+ KJ_DEFER(--slot->depth);
KJ_IF_MAYBE (pt, pool_perhaps) {
auto& pool_thread = static_cast<ProxyServer<Thread>&>(*pt);
MP_LOG(loop, Log::Debug)
@@ -236,8 +236,7 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
<< "IPC server error request #" << req << ", pool thread not found";
throw std::runtime_error("pool thread not found");
}
- })
- .attach(kj::mv(guard));
+ });
}
}, [&loop, req](::kj::Exception&& e) -> kj::Promise<typename ServerContext::CallContext> {
// If you see the error "(remote):0: failed: remote exception:However, this is a nit
There was a problem hiding this comment.
The diff above compiles and passes the tests, but so does this other:
diff --git a/include/mp/type-context.h b/include/mp/type-context.h
index d241d95..0a99610 100644
--- a/include/mp/type-context.h
+++ b/include/mp/type-context.h
@@ -223,7 +223,6 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
if (pool[i].depth < slot->depth) slot = &pool[i];
}
++slot->depth;
- auto guard = kj::defer([slot] { --slot->depth; });
return connection->m_threads.getLocalServer(slot->client)
.then([&loop, invoke = kj::mv(invoke), req](const kj::Maybe<Thread::Server&>& pool_perhaps) mutable {
KJ_IF_MAYBE (pt, pool_perhaps) {
@@ -236,8 +235,7 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
<< "IPC server error request #" << req << ", pool thread not found";
throw std::runtime_error("pool thread not found");
}
- })
- .attach(kj::mv(guard));
+ });
}
}, [&loop, req](::kj::Exception&& e) -> kj::Promise<typename ServerContext::CallContext> {
// If you see the error "(remote):0: failed: remote exception:baab0fe to
ac910d2
Compare
Perhaps as a follow up? Or at least I'd like to get a few approach ACKs so we know this design won't change |
ac910d2 to
baab0fe
Compare
Definite approach ACK for baab0fe & sorry I haven't reviewed this more closely yet. This is a simple and elegant way of adding a thread pool while changing existing code as little as possible. My one concern about this is I don't think scheduling algorithm is very robust. But it's probably good enough for practical purposes, and it can be improved later. An example of a problem would be that if that if there are 10 threads, and a client makes 100 method calls, and 99 of the method calls return instantly, but one call waits for a new block to be connected and takes minutes to return. In that case, some of the 99 method calls which could complete instantaneously will be stuck queued behind the one slow call, even though there are threads sitting idle which could execute them. To fix this, it would be best to just add all incoming requests to a single queue, and have threads read from the common queue, instead of giving each thread its own individual queue. I asked claude to implement this to get an idea of what it would look like bd1f80c (branch) but it's a more complicated approach and completely untested and I'd be happy to save something like this for a followup. Other more minor things:
Otherwise this looks very good as far as I can tell and I do want to get this reviewed and merged. |
baab0fe to
dd05eea
Compare
|
dd05eea removes |
|
@ryanofsky, r.e. second comment, is this what you mean? diff --git a/include/mp/type-context.h b/include/mp/type-context.h
index d241d95..9751de7 100644
--- a/include/mp/type-context.h
+++ b/include/mp/type-context.h
@@ -202,7 +202,10 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
Context::Reader context_arg = Accessor::get(params);
auto thread_client = context_arg.getThread();
auto* connection = server.m_context.connection;
- auto result = connection->m_threads.getLocalServer(thread_client)
+ auto thread_promise = context_arg.hasThread()
+ ? connection->m_threads.getLocalServer(thread_client)
+ : kj::Promise<kj::Maybe<Thread::Server&>>(nullptr);
+ auto result = thread_promise
.then([&loop, invoke = kj::mv(invoke), req, connection](const kj::Maybe<Thread::Server&>& perhaps) mutable {
// If the client specified a thread, dispatch to it directly.
KJ_IF_MAYBE (thread_server, perhaps) { |
That's close but not exactly what I mean. I'd like to preserve the current "invalid thread handle" error in the case where So idea would be more like |
dd05eea to
d77e897
Compare
|
Unfortunately a larger diff but all existing code moved into the |
|
Concept ACK This removes the requirement for non-libmultiprocess clients to manage thread handles explicitly. A server-wide threadpool is worth considering. The current per-connection pool allocates O(connections × pool_size) OS threads. External clients can open an unpredictable number of connections, so this scales without bound. A shared pool would cap total thread count at a fixed size across all connections. |
xyzconstant
left a comment
There was a problem hiding this comment.
Code review ACK d77e897
I think the changes are clean and ready to be merged.
Ran local and IPC tests on downstream, compiled a bitcoin-node instance to verify it wasn't affected, and finally tested the changes in a Rust client (see: xyzconstant/peer-observer@c414765).
This significantly improves how clients written in other languages consume libmultiprocess processes by eliminating the need to create a new thread for each IPC call, because the serving processes now automatically assign threads to requests with no threads specified.
I explored another alternative to the current approach but felt this was the most effective solution.
As a next step, based on ryanofsky's suggestion (comment), it might be helpful to rename the ThreadMap interface to something like Connection or Runtime for better clarity.
| # Pre-allocate a pool of server threads for implicit dispatch. When a | ||
| # request arrives with no context.thread set, the server dispatches it | ||
| # through this pool via a shared work queue. | ||
| makePool @1 (count :UInt32) -> (); |
There was a problem hiding this comment.
nit: Relocate the comment below makePool definition line to maintain consistency with the file.
|
Following on from my review:
I've made some progress on top of these changes. Happy to open a follow-up PR if/once this one gets merged. |
|
tACK d77e897 Tested with https://github.com/ViniciusCestarii/bitcoin-ipc-demo/tree/make-pool-tester and it successfully worked passing simultaneous calls to the threads with smaller queues. |
ryanofsky
left a comment
There was a problem hiding this comment.
Code review ACK d77e897. Looks good! Sorry for the delay getting to this and I plan to merge this soon after looking over the other review comments.
I still do have the same concern about scheduling algorithm not being very robust (#283 (comment)) since requests that should be fast could get stuck queued behind much slower requests and be delayed for a long time (potentially indefinitely) even if workers are free which could execute them. A good fix for this would be to add incoming requests to a shared queue and have workers read from the queue. But it sounds like current design does work well in practice, so I'm not very concerned about this and this could be improved in a followup
| const std::string thread_name = "pool/" + std::to_string(i); | ||
| std::promise<ThreadContext*> thread_context; | ||
| std::thread thread([&loop, &thread_context, thread_name]() { | ||
| g_thread_context.thread_name = ThreadName(loop.m_exe_name) + " (from " + thread_name + ")"; |
There was a problem hiding this comment.
In commit "Add makePool method on ThreadMap" (d77e897)
Not important would probably be a little better to drop "from " + here.
Format for client thread names is
client_exe_name-123/client_thread_name-456And for server thread names
server_exe_name-234/server_thread_name-567 (from client_exe_name-123/client_thread_name-456)Where numbers are process/thread ids. This code would make the server thread name look like:
server_exe_name-234/server_thread_name-567 (from pool/0)Which makes pool/0 look ilke a client process.
This patch introduces a pool of threads to the `Connection` class, and allows this pool to be populated with the thread map via `makePool`. When a client thread is not set in a request context, it is delegated to the pool. The pool is implemented as shortest-queue, where the thread with the shortest list of pending work handles the request. Tiebreaking is by lowest index. This was raised to me by Rust users, as they did not particularly care where work is executed on the server-side, but they have to set the thread regardless. ref: https://github.com/2140-dev/bitcoin-capnp-types/blob/master/tests/util/bitcoin_core.rs#L149
d77e897 to
0e49d91
Compare
|
Dropped FYI will not be able to rebase again until July 6th if more changes are required |
|
re-ACK 0e49d91 |
|
re-ACK 0e49d91 |
…ea02 16bf05dea02 Merge bitcoin-core/libmultiprocess#302: refactor: rename EventLoop::m_num_clients to m_num_refs dd537da9e40 Merge bitcoin-core/libmultiprocess#301: test: recursive async IPC calls and cleanups 400291de000 Merge bitcoin-core/libmultiprocess#299: ci: remove libevent from Core CIs 092be515adf Merge bitcoin-core/libmultiprocess#285: Add ReadList helper 5b617880c51 Merge bitcoin-core/libmultiprocess#283: Add `makePool` method on `ThreadMap` d4998304154 refactor: rename EventLoop::m_num_clients to m_num_refs 6450345c985 type: reserve first when reading std::unordered_set 4d0f8db5f99 proxy: add ReadList helper and dedup map/set/vector read handlers 0e49d911867 Add `makePool` method on `ThreadMap` 5519f7f9485 test: recursive async IPC calls a29ceff40bc ci: remove libevent from Core CIs 8412fcdc659 Merge bitcoin-core/libmultiprocess#295: Mark Waiter m_cv as guarded by m_mutex 1593ee2d18a Merge bitcoin-core/libmultiprocess#294: test: Add passDouble smoke test 9885d7dd33c Merge bitcoin-core/libmultiprocess#286: proxy-client: fix TSan data race in clientDestroy fa35501c4f0 Mark Waiter m_cv as guarded by m_mutex faaedb11f8a test: Add passDouble smoke test 733c64318d1 Merge bitcoin-core/libmultiprocess#292: type-number: fix clang-tidy modernize-use-nullptr 9cc3479ab33 Merge bitcoin-core/libmultiprocess#291: cmake: Add `mp_headers` custom target 201abd9e3a5 Merge bitcoin-core/libmultiprocess#289: cmake: make target_capnp_sources use CURRENT dirs 99820c8aecb Merge bitcoin-core/libmultiprocess#279: doc: Add comments to FIELD_* constants in proxy.h 73b985540c5 Merge bitcoin-core/libmultiprocess#278: doc: Fix and expand design.md e7e91b2e23e Merge bitcoin-core/libmultiprocess#277: Add std::unordered_set support and a helper BuildList to dedup list build handlers 91a951f59ac tidy fix: modernize-use-nullptr 16362f42d01 cmake: Add `mp_headers` custom target 615a94fe3a2 cmake: document ONLY_CAPNP option in target_capnp_sources 90982f75c6b mpgen: iwyu changes required by previous commit 25bb3e67f39 proxy-client: fix TSan data race in clientDestroy 620f297f311 cmake: make target_capnp_sources use CURRENT dirs 9de4b885aa6 test: use camelCase + $Proxy.name for FooStruct fields 011b91793dd type: add std::unordered_set support 20d19b9644e proxy: add BuildList helper and dedup map/set/vector build handlers e863c6cdf61 doc: Add comments to FIELD_* constants in proxy.h 18db0ab9570 doc: Fix and expand design.md 61de6975362 Merge bitcoin-core/libmultiprocess#273: proxy-client: tolerate exceptions from remote destroy during cleanup 9cec9d6ca55 Merge bitcoin-core/libmultiprocess#243: mpgen: support primitive std::optional struct fields 4aaff113745 Merge bitcoin-core/libmultiprocess#238: cmake, ci: updates for recent nixpkgs 2ac55a56b58 Merge bitcoin-core/libmultiprocess#218: Better error and log messages 6de92e1c732 proxy-client: tolerate exceptions from remote destroy during cleanup 90be8354d47 test: regression for ~ProxyClient destroy after peer disconnect 3c69d125a17 Merge bitcoin-core/libmultiprocess#260: event loop: tolerate unexpected exceptions in `post()` callbacks b8a48c65e60 event loop: tolerate unexpected exceptions in `post()` callbacks f787863d2cd Merge bitcoin-core/libmultiprocess#270: doc: Bump version 10 > 11 a22f6029103 doc: Bump version 10 > 11 4eae445d6d8 debug: Add TypeName() function and log statements for Proxy objects being created and destroyed f326c5b1b7b logging: Add better logging on IPC server-side failures 6dbfa56a040 mpgen: support primitive std::optional struct fields 8d1277deb55 mpgen refactor: add AccessorType function db716bbcba7 mpgen refactor: Move field handling code to FieldList class db7acb3ce27 ci: Fix shell.nix compatibility with CMake 4.0 91a7759a9ab cmake: Fix IWYU in nix by adding CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES git-subtree-dir: src/ipc/libmultiprocess git-subtree-split: 16bf05dea02651f75733ff08531181aa774fc5a8
…ol` method 6b0a907 Squashed 'src/ipc/libmultiprocess/' changes from 3edbe8f67c1..16bf05dea02 (Ryan Ofsky) Pull request description: The changes can be verified by running `test/lint/git-subtree-check.sh src/ipc/libmultiprocess` as described in [developer notes](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#subtrees) and [lint instructions](https://github.com/bitcoin/bitcoin/tree/master/test/lint#git-subtree-checksh). Changes since last subtree update (#34977): - 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](bitcoin-core/libmultiprocess#283)). - Adds `std::unordered_set` support, a `BuildList` helper, and a `ReadList` helper to reduce duplication in list build and read handlers ([#277](bitcoin-core/libmultiprocess#277), [#285](bitcoin-core/libmultiprocess#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](bitcoin-core/libmultiprocess#243)). - Produces more readable log output for Proxy object lifecycle events and IPC server-side failures ([#218](bitcoin-core/libmultiprocess#218)). - Handles exceptions thrown by `destroy` methods by logging instead of aborting ([#273](bitcoin-core/libmultiprocess#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](bitcoin-core/libmultiprocess#219)). - Handles unexpected exceptions thrown by callbacks (that should never happen) by logging errors instead of deadlocking ([#260](bitcoin-core/libmultiprocess#260)). - Fixes a rare mptest hang on musl builds caused by a lost wakeup bug in `Waiter` ([#295](bitcoin-core/libmultiprocess#295)). - Fixes a race condition in a log print detected by TSan ([#286](bitcoin-core/libmultiprocess#286)). - Build improvements: makes `target_capnp_sources` work correctly when libmultiprocess is used as a CMake subproject ([#289](bitcoin-core/libmultiprocess#289)), adds `mp_headers` target for better lint tool support ([#291](bitcoin-core/libmultiprocess#291)), and fixes compatibility with recent Nix and CMake 4.0 ([#238](bitcoin-core/libmultiprocess#238)). - Test, CI, documentation, and minor code improvements: design document corrections ([#278](bitcoin-core/libmultiprocess#278)), field constant comments ([#279](bitcoin-core/libmultiprocess#279)), clang-tidy fix ([#292](bitcoin-core/libmultiprocess#292)), new smoke test for double-precision float values ([#294](bitcoin-core/libmultiprocess#294)), new test for recursive async IPC calls ([#301](bitcoin-core/libmultiprocess#301)), removal of libevent from Core CI builds ([#299](bitcoin-core/libmultiprocess#299)), and rename of `EventLoop::m_num_clients` to `m_num_refs` ([#302](bitcoin-core/libmultiprocess#302)). ACKs for top commit: fanquake: ACK 02afa66 hebasto: ACK 02afa66. Tree-SHA512: ef81a951c971f328a0a98436030467eeea30925eb6016eafd9bc7a25726c87628a852bbb1d84b88bce340aeea2bed25c65bc55db1168ebcb850628cd18808883
This patch introduces a pool of threads to the
Connectionclass, and allows this pool to be populated with the thread map viamakePool. When a client thread is not set in a request context, it is delegated to the pool. This is unable to handle the guarentees with server-invoked callbacks that the current API offers, but these callbacks are not yet present in the interface.The pool is implemented as
round-robin as it is simplestshortest queue, but perhaps the pool could be a queue of requests with work-stealing for threads that are available.This was raised to me by Rust users, as they did not particularly care where work is executed on the server-side, but they have to set the thread regardless.
Tests in Rust can be seen here: 2140-dev/bitcoin-capnp-types#24
ref: https://github.com/2140-dev/bitcoin-capnp-types/blob/master/tests/util/bitcoin_core.rs#L149
ref: #281