feat(gateway): bound the executor and HTTP server thread pools#457
Open
bburda wants to merge 1 commit into
Open
feat(gateway): bound the executor and HTTP server thread pools#457bburda wants to merge 1 commit into
bburda wants to merge 1 commit into
Conversation
The rclcpp MultiThreadedExecutor and the cpp-httplib request pool both sized themselves to the host CPU count, so the gateway ran ~50 threads at idle and far more on many-core hosts for the same workload. Add two configurable, clamped parameters: - server.executor_threads (default 2): thread count for the main MultiThreadedExecutor. The executor only delivers the node's own callbacks plus the fast service-response callbacks; the blocking wait for an RPC runs on the cpp-httplib pool thread, not an executor thread, so a small executor cannot starve or deadlock RPC handlers. - server.http_thread_pool_size (default 3): a fixed-size cpp-httplib ThreadPool installed via new_task_queue, replacing max(8, cores - 1). Both are resolved through clamp_thread_count() into a bounded range ([1, 256] for the executor, [1, 1024] for the HTTP pool). Each active SSE stream pins one HTTP worker for its lifetime, so sse.max_clients now defaults to 2 (at or below the pool) to keep SSE from starving ordinary requests; raise both together for more concurrent streams. Add unit tests proving the pool caps concurrency at its size and that clamp_thread_count bounds the value, plus an integration test exercising the bounded pools alongside an open SSE stream. Closes #440
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces the gateway’s idle thread footprint by bounding both the ROS 2 MultiThreadedExecutor thread count and the cpp-httplib request worker pool to small, configurable sizes, with clamping to safe ranges to prevent misconfiguration. It also lowers the default sse.max_clients to avoid SSE streams consuming the entire (now smaller) HTTP worker pool.
Changes:
- Add
server.executor_threads(default 2, clamped to[1, 256]) and wire it intoMultiThreadedExecutor. - Add
server.http_thread_pool_size(default 3, clamped to[1, 1024]) and install a fixed-size cpp-httplibThreadPoolvianew_task_queue. - Lower default
sse.max_clientsfrom 10 to 2 and add unit + integration tests plus documentation describing the new parameters and their operational interactions.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_integration_tests/test/features/test_thread_pool_bounds.test.py | New launch test that verifies bounded pools still allow discovery/data sampling and that non-SSE requests succeed while an SSE stream is open. |
| src/ros2_medkit_gateway/test/test_http_server_thread_pool.cpp | New unit test proving the cpp-httplib pool caps handler concurrency and that clamp_thread_count() bounds values correctly. |
| src/ros2_medkit_gateway/src/main.cpp | Reads server.executor_threads, clamps it, and constructs the bounded MultiThreadedExecutor. |
| src/ros2_medkit_gateway/src/http/rest_server.cpp | Reads server.http_thread_pool_size, clamps it, and constructs HttpServerManager with a bounded request pool. |
| src/ros2_medkit_gateway/src/http/http_server.cpp | Implements applying a fixed-size httplib::ThreadPool via new_task_queue for both HTTP and HTTPS servers. |
| src/ros2_medkit_gateway/src/gateway_node.cpp | Declares new server thread-pool parameters and lowers default sse.max_clients to 2. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/thread_pool_config.hpp | Adds shared clamp_thread_count() helper for bounding operator-provided thread-count parameters. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/http_server.hpp | Extends HttpServerManager API to accept a thread-pool size and stores it for application at server start. |
| src/ros2_medkit_gateway/config/gateway_params.yaml | Documents and sets defaults for the new server pool parameters and the new sse.max_clients default. |
| src/ros2_medkit_gateway/CMakeLists.txt | Registers the new GTest target test_http_server_thread_pool. |
| docs/config/server.rst | Documents the new thread-pool parameters and updates sse.max_clients default and examples. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The rclcpp
MultiThreadedExecutorand the vendored cpp-httplib request pool both sized themselves to the host CPU count, so the gateway ran ~50 threads at idle (about 53 on a Nav2 stack) and far more on many-core hosts for the same workload. Under load, request p95 latency stayed low (~2.3 ms at 32 concurrent clients), so this was thread/scheduling overhead, not a latency problem.This bounds both pools and makes them configurable:
server.executor_threads(default2): thread count for the mainMultiThreadedExecutor, replacing rclcpp's default (host cores, min 2). The executor only delivers the node's own callbacks plus the fast service-response callbacks; the blocking wait for an RPC runs on the cpp-httplib pool thread, not an executor thread, so a small executor cannot starve or deadlock RPC handlers.server.http_thread_pool_size(default3): a fixed-size cpp-httplibThreadPoolinstalled vianew_task_queue, replacingmax(8, cores - 1).Both are resolved through a shared
clamp_thread_count()helper into a bounded range ([1, 256]for the executor,[1, 1024]for the HTTP pool) so a mis-set value can neither drop the pool to zero nor spawn a pathological thread count.Each active SSE stream pins one HTTP worker for its lifetime, so
sse.max_clientsdefault is lowered from 10 to 2 (kept at or below the pool) to keep SSE from starving ordinary requests. Raisehttp_thread_pool_sizeandsse.max_clientstogether for more concurrent streams.Issue
Type
Behavior change to surface:
sse.max_clientsdefault drops 10 -> 2, and both thread pools no longer scale with the host core count. Deployments relying on the old defaults should set the parameters explicitly.Testing
test_http_server_thread_poolstarts a real loopback server and proves the pool caps handler concurrency at its size (pool=1 serializes, pool=2 caps at 2), and thatclamp_thread_countfloors/caps out-of-range values. Full gateway unit suite green (2306 tests, 0 failures).test_thread_pool_boundslaunches the gateway with bounded pools and verifies discovery + data sampling work and that ordinary requests are still served while an SSE stream holds a pool worker. Existing SSE / auth / updates integration tests pass under the new defaults.Checklist
🤖 Generated with Claude Code