Skip to content

feat(gateway): bound the executor and HTTP server thread pools#457

Open
bburda wants to merge 1 commit into
mainfrom
feat/issue-440-bound-thread-pools
Open

feat(gateway): bound the executor and HTTP server thread pools#457
bburda wants to merge 1 commit into
mainfrom
feat/issue-440-bound-thread-pools

Conversation

@bburda

@bburda bburda commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

The rclcpp MultiThreadedExecutor and 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 (default 2): thread count for the main MultiThreadedExecutor, 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 (default 3): a fixed-size cpp-httplib ThreadPool installed via new_task_queue, replacing max(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_clients default is lowered from 10 to 2 (kept at or below the pool) to keep SSE from starving ordinary requests. Raise http_thread_pool_size and sse.max_clients together for more concurrent streams.


Issue


Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Behavior change to surface: sse.max_clients default 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

  • Unit (GTest): new test_http_server_thread_pool starts a real loopback server and proves the pool caps handler concurrency at its size (pool=1 serializes, pool=2 caps at 2), and that clamp_thread_count floors/caps out-of-range values. Full gateway unit suite green (2306 tests, 0 failures).
  • Integration (launch_testing): new test_thread_pool_bounds launches 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.
  • clang-format, cpplint, ament-copyright, flake8, and clang-tidy all clean.

Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Tests were added or updated if needed
  • Docs were updated if behavior or public API changed

🤖 Generated with Claude Code

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
Copilot AI review requested due to automatic review settings June 21, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 into MultiThreadedExecutor.
  • Add server.http_thread_pool_size (default 3, clamped to [1, 1024]) and install a fixed-size cpp-httplib ThreadPool via new_task_queue.
  • Lower default sse.max_clients from 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.

@bburda bburda self-assigned this Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bound the rclcpp executor and HTTP server thread pools

2 participants