Repository: ros2/rclcpp
Pull request #2378: use NodeParameterInterface instead of /parameter_event to update "use…
Author: login: fujitatomoya
Created: 2023-12-01T00:39:29Z | Merged: 2025-04-11T16:01:48Z

…_sim_time"

address https://github.com/ros2/rclcpp/issues/2370

depends on https://github.com/ros2/rclcpp/pull/2742


Diff:
diff --git a/rclcpp/src/rclcpp/time_source.cpp b/rclcpp/src/rclcpp/time_source.cpp
index 2ce5654e08..8c0126dcfb 100644
--- a/rclcpp/src/rclcpp/time_source.cpp
+++ b/rclcpp/src/rclcpp/time_source.cpp
@@ -276,13 +276,8 @@ class TimeSource::NodeState final
     on_set_parameters_callback_ = node_parameters_->add_on_set_parameters_callback(
       std::bind(&TimeSource::NodeState::on_set_parameters, this, std::placeholders::_1));
 
-
-    // TODO(tfoote) use parameters interface not subscribe to events via topic ticketed #609
-    parameter_subscription_ = rclcpp::AsyncParametersClient::on_parameter_event(
-      node_topics_,
-      [this](std::shared_ptr<const rcl_interfaces::msg::ParameterEvent> event) {
-        this->on_parameter_event(event);
-      });
+    post_set_parameters_callback_ = node_parameters_->add_post_set_parameters_callback(
+      std::bind(&TimeSource::NodeState::post_set_parameters, this, std::placeholders::_1));
   }
 
   // Detach the attached node
@@ -296,8 +291,11 @@ class TimeSource::NodeState final
     if (on_set_parameters_callback_) {
       node_parameters_->remove_on_set_parameters_callback(on_set_parameters_callback_.get());
     }
+    if (post_set_parameters_callback_) {
+      node_parameters_->remove_post_set_parameters_callback(post_set_parameters_callback_.get());
+    }
     on_set_parameters_callback_.reset();
-    parameter_subscription_.reset();
+    post_set_parameters_callback_.reset();
     node_base_.reset();
     node_topics_.reset();
     node_graph_.reset();
@@ -436,9 +434,9 @@ class TimeSource::NodeState final
   // On set Parameters callback handle
   node_interfaces::OnSetParametersCallbackHandle::SharedPtr on_set_parameters_callback_{nullptr};
 
-  // Parameter Event subscription
-  using ParamSubscriptionT = rclcpp::Subscription<rcl_interfaces::msg::ParameterEvent>;
-  std::shared_ptr<ParamSubscriptionT> parameter_subscription_;
+  // Post set Parameters callback handle
+  node_interfaces::PostSetParametersCallbackHandle::SharedPtr
+    post_set_parameters_callback_{nullptr};
 
   // Callback for parameter settings
   rcl_interfaces::msg::SetParametersResult on_set_parameters(
@@ -461,52 +459,27 @@ class TimeSource::NodeState final
     return result;
   }
 
-  // Callback for parameter updates
-  void on_parameter_event(std::shared_ptr<const rcl_interfaces::msg::ParameterEvent> event)
+  // Callback for post parameter updates
+  void post_set_parameters(const std::vector<rclcpp::Parameter> & parameters)
   {
-    std::lock_guard<std::mutex> guard(node_base_lock_);
-
-    if (node_base_ == nullptr) {
-      // Do nothing if node_base_ is nullptr because it means the TimeSource is now
-      // without an attached node
-      return;
-    }
-
-    // Filter out events on 'use_sim_time' parameter instances in other nodes.
-    if (event->node != node_base_->get_fully_qualified_name()) {
-      return;
-    }
-    // Filter for only 'use_sim_time' being added or changed.
-    rclcpp::ParameterEventsFilter filter(event, {"use_sim_time"},
-      {rclcpp::ParameterEventsFilter::EventType::NEW,
-        rclcpp::ParameterEventsFilter::EventType::CHANGED});
-    for (auto & it : filter.get_events()) {
-      if (it.second->value.type != ParameterType::PARAMETER_BOOL) {
-        RCLCPP_ERROR(logger_, "use_sim_time parameter cannot be set to anything but a bool");
-        continue;
-      }
-      if (it.second->value.bool_value) {
-        parameter_state_ = SET_TRUE;
-        clocks_state_.enable_ros_time();
-        create_clock_sub();
-      } else {
-        parameter_state_ = SET_FALSE;
-        destroy_clock_sub();
-        clocks_state_.disable_ros_time();
+    // "use_sim_time" has been set, so just applys it to internal states
+    for (const auto & param : parameters) {
+      if (param.get_name() == "use_sim_time") {
+        if (param.as_bool()) {
+          parameter_state_ = SET_TRUE;
+          clocks_state_.enable_ros_time();
+          create_clock_sub();
+        } else {
+          parameter_state_ = SET_FALSE;
+          destroy_clock_sub();
+          clocks_state_.disable_ros_time();
+        }
       }
     }
-    // Handle the case that use_sim_time was deleted.
-    rclcpp::ParameterEventsFilter deleted(event, {"use_sim_time"},
-      {rclcpp::ParameterEventsFilter::EventType::DELETED});
-    for (auto & it : deleted.get_events()) {
-      (void) it;  // if there is a match it's already matched, don't bother reading it.
-      // If the parameter is deleted mark it as unset but don't change state.
-      parameter_state_ = UNSET;
-    }
   }
 
   // An enum to hold the parameter state
-  enum UseSimTimeParameterState {UNSET, SET_TRUE, SET_FALSE};
+  enum UseSimTimeParameterState {SET_TRUE, SET_FALSE};
   UseSimTimeParameterState parameter_state_;
 };
 
diff --git a/rclcpp/test/rclcpp/test_executor.cpp b/rclcpp/test/rclcpp/test_executor.cpp
index 21a19cee18..400a2db5f6 100644
--- a/rclcpp/test/rclcpp/test_executor.cpp
+++ b/rclcpp/test/rclcpp/test_executor.cpp
@@ -19,11 +19,16 @@
 #include <stdexcept>
 #include <string>
 
+#include "rcl_interfaces/msg/parameter_event.hpp"
+
 #include "rclcpp/executor.hpp"
 #include "rclcpp/memory_strategy.hpp"
 #include "rclcpp/executors/single_threaded_executor.hpp"
+#include "rclcpp/rclcpp.hpp"
 #include "rclcpp/strategies/allocator_memory_strategy.hpp"
 
+#include "test_msgs/msg/empty.hpp"
+
 #include "../mocking_utils/patch.hpp"
 #include "../utils/rclcpp_gtest_macros.hpp"
 
@@ -331,8 +336,17 @@ TEST_F(TestExecutor, spin_all_fail_wait_set_clear) {
     node->create_wall_timer(std::chrono::milliseconds(1), [&]() {});
 
   dummy.add_node(node);
+  auto callback = [this](test_msgs::msg::Empty::ConstSharedPtr) {};
+  rclcpp::Subscription<test_msgs::msg::Empty>::SharedPtr subscription;
+  auto node_topics = node->get_node_topics_interface();
+  subscription =
+    rclcpp::create_subscription<test_msgs::msg::Empty>(
+    node_topics, "test", rclcpp::QoS(10), std::move(callback));
   auto mock = mocking_utils::patch_and_return("lib:rclcpp", rcl_wait_set_clear, RCL_RET_ERROR);
 
+  dummy.spin_all(std::chrono::milliseconds(1));
+  // second spin_all triggers rcl_wait_set_clear that should be called
+  // whenever a waitset gets rebuild and it was not changed in size.
   RCLCPP_EXPECT_THROW_EQ(
     dummy.spin_all(std::chrono::milliseconds(1)),
     std::runtime_error("Couldn't clear the wait set: error not set"));
@@ -354,8 +368,14 @@ TEST_F(TestExecutor, spin_some_fail_wait_set_resize) {
 TEST_F(TestExecutor, spin_some_fail_add_handles_to_wait_set) {
   DummyExecutor dummy;
   auto node = std::make_shared<rclcpp::Node>("node", "ns");
-  auto timer =
-    node->create_wall_timer(std::chrono::milliseconds(1), [&]() {});
+
+  // create subscription explicitly, because we do not create subscription
+  // on /parameter_events for 'use_sim_time' parameter anymore.
+  auto callback = [this](test_msgs::msg::Empty::ConstSharedPtr) {};
+  rclcpp::Subscription<test_msgs::msg::Empty>::SharedPtr subscription;
+  subscription =
+    node->create_subscription<test_msgs::msg::Empty>(
+    "test", rclcpp::QoS(10), std::move(callback));
 
   dummy.add_node(node);
   auto mock = mocking_utils::patch_and_return(


Reviews:
user: login: fujitatomoya
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2023-12-01T00:49:23Z
user: login: jmachowinski
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T08:50:43Z
user: login: fujitatomoya
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T19:23:46Z
user: login: fujitatomoya
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T19:25:33Z
user: login: jmachowinski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T16:05:38Z

Review comments:
diff_hunk: @@ -276,13 +276,8 @@ class TimeSource::NodeState final
     on_set_parameters_callback_ = node_parameters_->add_on_set_parameters_callback(
       std::bind(&TimeSource::NodeState::on_set_parameters, this, std::placeholders::_1));
 
-
-    // TODO(tfoote) use parameters interface not subscribe to events via topic ticketed #609
-    parameter_subscription_ = rclcpp::AsyncParametersClient::on_parameter_event(
-      node_topics_,
-      [this](std::shared_ptr<const rcl_interfaces::msg::ParameterEvent> event) {
-        this->on_parameter_event(event);
-      });
+    post_set_parameters_callback_ = node_parameters_->add_post_set_parameters_callback(
+      std::bind(&TimeSource::NodeState::post_set_parameters, this, std::placeholders::_1));
path: rclcpp/src/rclcpp/time_source.cpp
user: login: fujitatomoya
body: using NodeParameterInterface instead of `/parameter_event` provides significant efficiency especially ROS 2 system has many nodes in the network. with this fix, we do not need to receive all the events and filter the `node` and `use_sim_time` parameter from `/parameter_event` topic.
created_at: 2023-12-01T00:43:23Z
author_association: COLLABORATOR
diff_hunk: @@ -579,7 +579,7 @@ NodeParameters::declare_parameter(
   bool ignore_override)
 {
   std::lock_guard<std::recursive_mutex> lock(mutex_);
-  ParameterMutationRecursionGuard guard(parameter_modification_enabled_);
+  // ParameterMutationRecursionGuard guard(parameter_modification_enabled_);
path: rclcpp/src/rclcpp/node_interfaces/node_parameters.cpp
user: login: fujitatomoya
body: this recursion guard is block this PR work.

https://github.com/ros2/rclcpp/blob/26f6efa840cbac047a93dc21d055cfe9cde7270f/rclcpp/include/rclcpp/node_interfaces/node_parameters.hpp#L58-L80

i guess this is not only for this PR's blocker, but also when we create either `publisher` or `subscription` with overriding the QoS parameters enabled via parameter callback. i guess it is something user would do when the specific parameter is enabled, and then create the `subscription` with QoS override. this will be generating `rclcpp::exceptions::ParameterModifiedInCallbackException` because it eventually call `rclcpp::detail::declare_qos_parameters`.

i will submit the issue to open this discussion.
created_at: 2023-12-01T00:49:00Z
author_association: COLLABORATOR
diff_hunk: @@ -331,6 +336,10 @@ TEST_F(TestExecutor, spin_all_fail_wait_set_clear) {
     node->create_wall_timer(std::chrono::milliseconds(1), [&]() {});
 
   dummy.add_node(node);
+  auto callback = [](rcl_interfaces::msg::ParameterEvent::ConstSharedPtr) {};
+  auto event_sub = rclcpp::AsyncParametersClient::on_parameter_event(
+      node->get_node_topics_interface(),
+      callback);
path: rclcpp/test/rclcpp/test_executor.cpp
user: login: fujitatomoya
body: @jmachowinski i need your help here... 🙇 since it does not create the `/parameter_events` subscription via node topic interface on node creation internally, it does not hit `rcl_wait_set_clear` and raise the expected exception.

this does not work either.

```suggestion
  auto callback = [this](test_msgs::msg::Empty::ConstSharedPtr) {};
  rclcpp::Subscription<test_msgs::msg::Empty>::SharedPtr subscription;
  auto node_topics = node->get_node_topics_interface();
  subscription =
    rclcpp::create_subscription<test_msgs::msg::Empty>(
      node_topics, "test", rclcpp::QoS(10), std::move(callback));
```

Do you have idea to make this test pass?
created_at: 2025-04-09T19:23:12Z
author_association: COLLABORATOR
diff_hunk: @@ -357,6 +366,14 @@ TEST_F(TestExecutor, spin_some_fail_add_handles_to_wait_set) {
   auto timer =
     node->create_wall_timer(std::chrono::milliseconds(1), [&]() {});
path: rclcpp/test/rclcpp/test_executor.cpp
user: login: fujitatomoya
body: this test was dependent on the subscription for `/parameter_events`, maybe this timer should be removed?
created_at: 2025-04-09T19:25:05Z
author_association: COLLABORATOR
diff_hunk: @@ -331,6 +336,10 @@ TEST_F(TestExecutor, spin_all_fail_wait_set_clear) {
     node->create_wall_timer(std::chrono::milliseconds(1), [&]() {});
 
   dummy.add_node(node);
+  auto callback = [](rcl_interfaces::msg::ParameterEvent::ConstSharedPtr) {};
+  auto event_sub = rclcpp::AsyncParametersClient::on_parameter_event(
+      node->get_node_topics_interface(),
+      callback);
path: rclcpp/test/rclcpp/test_executor.cpp
user: login: jmachowinski
body: either waiting longer, or spinning a second time should trigger this. rcl_wait_set_clear should be called, whenever a waitset gets rebuild, and it was not changed in size. Normally this happens after an event was processed, and we are waiting for any further event.
created_at: 2025-04-10T16:05:38Z
author_association: COLLABORATOR

PR comments:
user: login: fujitatomoya
created_at: 2023-12-01T00:49:40Z
body: CC: @Barry-Xu-2018 @iuhilnehc-ynos 
author_association: COLLABORATOR
user: login: jmachowinski
created_at: 2025-04-09T08:55:18Z
body: Looks good to me. While reviewing this, I stumbled over 
https://github.com/ros2/rclcpp/blob/892cae99159597b85d3e569953ed67652a0c9eed/rclcpp/src/rclcpp/time_source.cpp#L400

This could also be simplified by just spinning. The future has no way of waking up the executor, and the cancel:
https://github.com/ros2/rclcpp/blob/892cae99159597b85d3e569953ed67652a0c9eed/rclcpp/src/rclcpp/time_source.cpp#L433
Is what stops the thread in the end...
I really don't like this spin until future complete api, I have seen a lot of wrong usages...
author_association: COLLABORATOR
user: login: ahcorde
created_at: 2025-04-09T09:34:07Z
body: Pulls: ros2/rclcpp#2378
Gist: https://gist.githubusercontent.com/ahcorde/79e6ba5c051490b0daa01133625efc73/raw/0e41a6ea5e570ef85bb5bc4d62ab11b03e90532d/ros2.repos
BUILD args:  --packages-above-and-dependencies rclcpp
TEST args:  --packages-above rclcpp
ROS Distro: rolling
Job: ci_launcher
ci_launcher ran: https://ci.ros2.org/job/ci_launcher/15636
* Linux [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux&build=23089)](http://ci.ros2.org/job/ci_linux/23089/)
* Linux-aarch64 [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-aarch64&build=17308)](http://ci.ros2.org/job/ci_linux-aarch64/17308/)
* Linux-rhel [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-rhel&build=2566)](http://ci.ros2.org/job/ci_linux-rhel/2566/)
* Windows [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_windows&build=23966)](http://ci.ros2.org/job/ci_windows/23966/)
author_association: CONTRIBUTOR
user: login: fujitatomoya
created_at: 2025-04-09T15:53:25Z
body: @ahcorde @jmachowinski thanks for reviewing this. i need to rebase and take care of the failures.
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-09T16:33:33Z
body: > Looks good to me. While reviewing this, I stumbled over
> 
> https://github.com/ros2/rclcpp/blob/892cae99159597b85d3e569953ed67652a0c9eed/rclcpp/src/rclcpp/time_source.cpp#L400
> 
> This could also be simplified by just spinning. The future has no way of waking up the executor, and the cancel:
> 
> https://github.com/ros2/rclcpp/blob/892cae99159597b85d3e569953ed67652a0c9eed/rclcpp/src/rclcpp/time_source.cpp#L433
> 
> 
> Is what stops the thread in the end...
> I really don't like this spin until future complete api, I have seen a lot of wrong usages...

@jmachowinski agree, i can patch that with another PR (and probably backports), because this one is dependent on current rolling only. i will include you once that is ready.
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-11T03:59:25Z
body: Pulls: ros2/rclcpp#2378
Gist: https://gist.githubusercontent.com/fujitatomoya/5c5385f4f86450bdac3203a85de2770f/raw/0e41a6ea5e570ef85bb5bc4d62ab11b03e90532d/ros2.repos
BUILD args:  --packages-above-and-dependencies rclcpp
TEST args:  --packages-above rclcpp
ROS Distro: rolling
Job: ci_launcher
ci_launcher ran: https://ci.ros2.org/job/ci_launcher/15661
* Linux [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux&build=23121)](http://ci.ros2.org/job/ci_linux/23121/)
* Linux-aarch64 [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-aarch64&build=17335)](http://ci.ros2.org/job/ci_linux-aarch64/17335/)
* Linux-rhel [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-rhel&build=2593)](http://ci.ros2.org/job/ci_linux-rhel/2593/)
* Windows [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_windows&build=23993)](http://ci.ros2.org/job/ci_windows/23993/)
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-11T15:46:29Z
body: windows failure is unrelated.
author_association: COLLABORATOR