From 72e91a3393585ca2c6baf26b61606ddee9540586 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sat, 26 Sep 2026 09:45:31 +0200 Subject: [PATCH 1/5] fix: keep the gateway's helper nodes off the gateway's own name launch_ros Node(name=...) passes -r __node:= to the whole process. The fault-service clients, the lifecycle state reader and the subscription executor each create an rclcpp::Node that honoured that rule, so one gateway showed up as four nodes named /ros2_medkit_gateway: ros2 node list warned about duplicate names, rosout logged "Publisher already registered" three times, and ros2 param list printed every parameter four times. The helpers are now built by make_helper_node(): - named _, so they are hidden from ros2 node list and from discovery (no new Apps; count_peer_nodes no longer needs the helper exception list); - a node-local __node remap overrides the process-wide one. The other process arguments (namespace, remaps, parameter files) still apply; - use_sim_time is copied from the gateway, because a parameters file keyed by the gateway's name does not match the helper; - no parameter services or parameter event publisher: the fault-client node is spun only during its own requests, and a get_parameters call to it timed out; the lifecycle reader has the same shape. New integration tests launch the gateway through launch_ros, once plainly and once in a namespace with use_sim_time and with remap rules that are the only route to the fault manager. They check a single gateway node in the graph, ros2 param list without duplicates, unchanged /apps, hidden helpers in the gateway namespace, /clock subscriptions on the helpers under sim time, and that faults, topic samples and lifecycle states still work. The graph watchdog compiles the lifecycle state reader in, so it now compiles helper_node.cpp too. test_gateway_node.cpp drops redundant c_str() calls flagged by clang-tidy. --- docs/api/rest.rst | 6 +- docs/config/discovery-options.rst | 4 +- src/ros2_medkit_gateway/CMakeLists.txt | 6 + .../design/ros2_subscription_architecture.rst | 8 +- .../ros2_medkit_gateway/gateway_node.hpp | 4 +- .../ros2_common/helper_node.hpp | 39 ++++ .../ros2_subscription_executor.hpp | 5 +- src/ros2_medkit_gateway/src/gateway_node.cpp | 8 +- .../status/ros2_lifecycle_state_reader.cpp | 4 +- .../ros2_fault_service_transport.cpp | 3 +- .../src/ros2_common/helper_node.cpp | 35 ++++ .../ros2_subscription_executor.cpp | 8 +- .../test/test_gateway_node.cpp | 157 +++++++------- .../test/test_helper_node.cpp | 118 +++++++++++ .../test/test_ros2_subscription_executor.cpp | 3 +- .../ros2_medkit_test_utils/launch_helpers.py | 23 +- .../ros2_medkit_test_utils/ros_graph.py | 108 ++++++++++ .../test_gateway_helper_nodes.test.py | 166 +++++++++++++++ ...st_gateway_helper_nodes_namespaced.test.py | 197 ++++++++++++++++++ .../ros2_medkit_graph_watchdog/CMakeLists.txt | 3 +- 20 files changed, 797 insertions(+), 108 deletions(-) create mode 100644 src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/helper_node.hpp create mode 100644 src/ros2_medkit_gateway/src/ros2_common/helper_node.cpp create mode 100644 src/ros2_medkit_gateway/test/test_helper_node.cpp create mode 100644 src/ros2_medkit_integration_tests/ros2_medkit_test_utils/ros_graph.py create mode 100644 src/ros2_medkit_integration_tests/test/features/test_gateway_helper_nodes.test.py create mode 100644 src/ros2_medkit_integration_tests/test/features/test_gateway_helper_nodes_namespaced.test.py diff --git a/docs/api/rest.rst b/docs/api/rest.rst index 5f55fb862..fb4e9c678 100644 --- a/docs/api/rest.rst +++ b/docs/api/rest.rst @@ -229,9 +229,9 @@ Server Capabilities "/powertrain/engine/rpm_sensor", "/ros2_medkit_gateway", "/_param_client_node", - "/ros2_medkit_gateway_fault_clients", - "/ros2_medkit_gateway_lifecycle_state_reader", - "/ros2_medkit_gateway_sub" + "/_ros2_medkit_gateway_fault_clients", + "/_ros2_medkit_gateway_lifecycle_state_reader", + "/_ros2_medkit_gateway_sub" ], "peer_names": [] } diff --git a/docs/config/discovery-options.rst b/docs/config/discovery-options.rst index c47613dc0..22cf29310 100644 --- a/docs/config/discovery-options.rst +++ b/docs/config/discovery-options.rst @@ -81,7 +81,9 @@ Internal Node Filtering When ``filter_internal_nodes`` is true (the default), ROS 2 nodes whose names start with an underscore (``_``) are excluded from the entity tree. This filters out ROS 2 internal infrastructure nodes such as ``_ros2cli_*``, -``_param_client_node``, and similar system nodes that should not appear as +``_param_client_node``, the gateway's own helper nodes +(``__fault_clients``, ``__lifecycle_state_reader``, +``__sub``), and similar system nodes that should not appear as SOVD entities. The filter applies to both locally discovered Apps and peer-discovered Apps (after stripping the peer prefix). diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index ca40feb9a..55447235a 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -210,6 +210,7 @@ add_library(gateway_ros2 STATIC src/plugins/plugin_loader.cpp src/plugins/plugin_manager.cpp src/ros2_common/callback_groups.cpp + src/ros2_common/helper_node.cpp src/ros2_common/ros2_subscription_executor.cpp src/ros2_common/ros2_subscription_slot.cpp src/script_manager.cpp @@ -882,6 +883,11 @@ if(BUILD_TESTING) target_link_libraries(test_ros2_lifecycle_state_reader gateway_ros2) medkit_target_dependencies(test_ros2_lifecycle_state_reader rclcpp lifecycle_msgs) + # Helper nodes keep their own name under a process-wide __node remap + medkit_add_gtest(test_helper_node test/test_helper_node.cpp) + target_link_libraries(test_helper_node gateway_ros2) + medkit_target_dependencies(test_helper_node rclcpp) + # Private client nodes torn down after a first graph wait that follows rclcpp::shutdown() medkit_add_gtest(test_graph_listener_join test/test_graph_listener_join.cpp) target_link_libraries(test_graph_listener_join gateway_ros2) diff --git a/src/ros2_medkit_gateway/design/ros2_subscription_architecture.rst b/src/ros2_medkit_gateway/design/ros2_subscription_architecture.rst index e6b2eb007..15ce0dc86 100644 --- a/src/ros2_medkit_gateway/design/ros2_subscription_architecture.rst +++ b/src/ros2_medkit_gateway/design/ros2_subscription_architecture.rst @@ -68,8 +68,12 @@ Ros2SubscriptionExecutor Owns: - One dedicated ``std::thread`` (the worker). -- One ``rclcpp::Node`` (the subscription node, suffixed ``_sub``) exclusively - owned by this executor's internal ``SingleThreadedExecutor``. The gateway's +- One ``rclcpp::Node`` (the subscription node, ``__sub``) + exclusively owned by this executor's internal ``SingleThreadedExecutor``. + It is created by ``make_helper_node`` (``ros2_common/helper_node.hpp``): + hidden by the leading underscore, in the gateway's namespace, with the + gateway's ``use_sim_time`` and a node-local name remap, so a launch_ros + ``Node(name=...)`` does not rename it to the gateway's own name. The gateway's main ``MultiThreadedExecutor`` never sees this node; creation, destruction and callback dispatch of every subscription on it run on the single worker thread, preserving the single-writer invariant against rcl's hash-map. diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp index a46a11bf5..6c54b11d3 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/gateway_node.hpp @@ -73,8 +73,8 @@ class GatewayNode : public rclcpp::Node { ~GatewayNode() override; /// Count application (peer) nodes from (name, namespace) pairs: excludes hidden - /// nodes and the gateway's own nodes (whose FQN starts with @p self_fqn). Static - /// and public so the startup-summary counting logic is unit-testable. + /// nodes (the gateway's helper nodes are hidden) and the node at @p self_fqn. + /// Static and public so the startup-summary counting logic is unit-testable. static size_t count_peer_nodes(const std::vector> & nodes_and_namespaces, const std::string & self_fqn); diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/helper_node.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/helper_node.hpp new file mode 100644 index 000000000..132bdd010 --- /dev/null +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/helper_node.hpp @@ -0,0 +1,39 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include + +namespace ros2_medkit_gateway::ros2_common { + +/** + * @brief Create an in-process helper node of @p host, in the host's namespace and context. + * + * The helper is named `_`. The leading underscore hides it from + * `ros2 node list` and from discovery. A node-local `__node` remap keeps a + * process-wide `-r __node:=` (added by launch_ros `Node(name=...)`) from + * giving the helper the host's name. All other process arguments still apply. + * `use_sim_time` is copied from the host, because a parameters file keyed by the + * host's name does not match the helper. + * + * The helper has no parameter services and no parameter event publisher. A helper + * spun only during its own requests would leave those services unanswered. + */ +std::shared_ptr make_helper_node(rclcpp::Node & host, const std::string & suffix); + +} // namespace ros2_medkit_gateway::ros2_common diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/ros2_subscription_executor.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/ros2_subscription_executor.hpp index 6cc7a240a..19541d259 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/ros2_subscription_executor.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/ros2_common/ros2_subscription_executor.hpp @@ -83,6 +83,7 @@ class Ros2SubscriptionExecutor final { std::chrono::milliseconds watchdog_threshold; std::chrono::milliseconds watchdog_tick; std::chrono::milliseconds graph_poll_tick; + /// The subscription node is named `_`. std::string subscription_node_name_suffix; // Explicit ctor needed because GCC does not allow default member initializers @@ -121,8 +122,8 @@ class Ros2SubscriptionExecutor final { * deliberately not wired up - sharing the subscription node with a multi-threaded * executor reintroduces the rcutils_hash_map race this class exists to eliminate. * - * @param gateway_node Owning gateway node. Used only to derive the subscription - * node name and namespace; no references retained after + * @param gateway_node Owning gateway node. Used only to set up the subscription + * node (see make_helper_node); no references retained after * construction. * @param cfg Bounded resource configuration. */ diff --git a/src/ros2_medkit_gateway/src/gateway_node.cpp b/src/ros2_medkit_gateway/src/gateway_node.cpp index b17256644..1f52448e4 100644 --- a/src/ros2_medkit_gateway/src/gateway_node.cpp +++ b/src/ros2_medkit_gateway/src/gateway_node.cpp @@ -1601,17 +1601,15 @@ size_t GatewayNode::count_peer_nodes(const std::vector