Repository: ros2/rclcpp
Pull request #2742: enable parameter update recursively only when QoS override parameters.
Author: login: fujitatomoya
Created: 2025-02-12T04:41:28Z | Merged: 2025-04-09T09:32:11Z

closes https://github.com/ros2/rclcpp/issues/2741


Diff:
diff --git a/rclcpp/include/rclcpp/detail/qos_parameters.hpp b/rclcpp/include/rclcpp/detail/qos_parameters.hpp
index 651e58e7d2..1a9138e9c8 100644
--- a/rclcpp/include/rclcpp/detail/qos_parameters.hpp
+++ b/rclcpp/include/rclcpp/detail/qos_parameters.hpp
@@ -97,6 +97,9 @@ declare_parameter_or_get(
   rcl_interfaces::msg::ParameterDescriptor descriptor)
 {
   try {
+    // enable parameter modification to make it possible
+    // to declare QoS override parameters during parameter callbacks.
+    parameters_interface.enable_parameter_modification();
     return parameters_interface.declare_parameter(
       param_name, param_value, descriptor);
   } catch (const rclcpp::exceptions::ParameterAlreadyDeclaredException &) {
diff --git a/rclcpp/include/rclcpp/node_interfaces/node_parameters.hpp b/rclcpp/include/rclcpp/node_interfaces/node_parameters.hpp
index ffbb400c11..3d867090dd 100644
--- a/rclcpp/include/rclcpp/node_interfaces/node_parameters.hpp
+++ b/rclcpp/include/rclcpp/node_interfaces/node_parameters.hpp
@@ -214,6 +214,10 @@ class NodeParameters : public NodeParametersInterface
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const override;
 
+  RCLCPP_PUBLIC
+  void
+  enable_parameter_modification() override;
+
   using PreSetCallbacksHandleContainer = std::list<PreSetParametersCallbackHandle::WeakPtr>;
   using OnSetCallbacksHandleContainer = std::list<OnSetParametersCallbackHandle::WeakPtr>;
   using PostSetCallbacksHandleContainer = std::list<PostSetParametersCallbackHandle::WeakPtr>;
diff --git a/rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp b/rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
index 1cf10c1a97..8636fdec74 100644
--- a/rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
+++ b/rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
@@ -270,6 +270,26 @@ class NodeParametersInterface
   virtual
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const = 0;
+
+  /// Enable parameter modification recursively during parameter callbacks.
+  /**
+   * This function is used to enable parameter modification during parameter callbacks.
+   *
+   * There are times when it does not allow parameter modification, such as when the parameter
+   * callbacks are being called and tries to modify the parameters with set_parameter and
+   * declare_parameter to avoid recursive parameter modification.
+   * This is protected by rclcpp::node_interfaces::ParameterMutationRecursionGuard.
+   *
+   * This function is explicitly called to allow the recursive parameter operation during
+   * parameter callbacks by the application.
+   * Once it is enabled, the next parameter operation set/declare/undeclare_parameter are
+   * allowed to execute in the parameter callback. But, no more further recursive operation
+   * is allowed, unless user application calls this API again.
+   */
+  RCLCPP_PUBLIC
+  virtual
+  void
+  enable_parameter_modification() = 0;
 };
 
 }  // namespace node_interfaces
diff --git a/rclcpp/src/rclcpp/node_interfaces/node_parameters.cpp b/rclcpp/src/rclcpp/node_interfaces/node_parameters.cpp
index 922ce9e4d1..33f9391917 100644
--- a/rclcpp/src/rclcpp/node_interfaces/node_parameters.cpp
+++ b/rclcpp/src/rclcpp/node_interfaces/node_parameters.cpp
@@ -1191,3 +1191,10 @@ NodeParameters::get_parameter_overrides() const
 {
   return parameter_overrides_;
 }
+
+void
+NodeParameters::enable_parameter_modification()
+{
+  std::lock_guard<std::recursive_mutex> lock(mutex_);
+  parameter_modification_enabled_ = true;
+}
diff --git a/rclcpp/test/rclcpp/CMakeLists.txt b/rclcpp/test/rclcpp/CMakeLists.txt
index b7f744f049..3ae3fa7306 100644
--- a/rclcpp/test/rclcpp/CMakeLists.txt
+++ b/rclcpp/test/rclcpp/CMakeLists.txt
@@ -196,7 +196,7 @@ ament_add_gtest(test_node_interfaces__node_parameters
   node_interfaces/test_node_parameters.cpp)
 ament_add_test_label(test_node_interfaces__node_parameters mimick)
 if(TARGET test_node_interfaces__node_parameters)
-  target_link_libraries(test_node_interfaces__node_parameters ${PROJECT_NAME} mimick rcpputils::rcpputils)
+  target_link_libraries(test_node_interfaces__node_parameters ${PROJECT_NAME} mimick rcpputils::rcpputils ${test_msgs_TARGETS})
 endif()
 ament_add_gtest(test_node_interfaces__node_services
   node_interfaces/test_node_services.cpp)
diff --git a/rclcpp/test/rclcpp/node_interfaces/test_node_parameters.cpp b/rclcpp/test/rclcpp/node_interfaces/test_node_parameters.cpp
index 0cc06792a8..5e851a8434 100644
--- a/rclcpp/test/rclcpp/node_interfaces/test_node_parameters.cpp
+++ b/rclcpp/test/rclcpp/node_interfaces/test_node_parameters.cpp
@@ -29,6 +29,8 @@
 #include "rclcpp/node.hpp"
 #include "rclcpp/node_interfaces/node_parameters.hpp"
 
+#include "test_msgs/msg/empty.hpp"
+
 #include "../../mocking_utils/patch.hpp"
 #include "../../utils/rclcpp_gtest_macros.hpp"
 
@@ -329,6 +331,89 @@ TEST_F(TestNodeParameters, add_remove_post_set_parameters_callback) {
     std::runtime_error("Post set parameter callback doesn't exist"));
 }
 
+TEST_F(TestNodeParameters, set_param_recursive_in_post_set_parameters_callback) {
+  rclcpp::Subscription<test_msgs::msg::Empty>::SharedPtr subscription_;
+  rclcpp::Publisher<test_msgs::msg::Empty>::SharedPtr publisher_;
+
+  rcl_interfaces::msg::ParameterDescriptor param_descriptor;
+  param_descriptor.name = "create_entities";
+  param_descriptor.type = rcl_interfaces::msg::ParameterType::PARAMETER_BOOL;
+  param_descriptor.read_only = false;
+
+  bool result = node_parameters->declare_parameter(
+    "create_entities", rclcpp::ParameterValue(false), param_descriptor, false).get<bool>();
+  EXPECT_EQ(result, false);
+
+  // Register a callback to create/delete publisher and subscription with
+  // QoS override parameter options. This will call declare_parameter recursively
+  // during this callback.
+  auto sub_callback = [](test_msgs::msg::Empty::ConstSharedPtr) {};
+  auto callback = [&](const std::vector<rclcpp::Parameter> & parameters) {
+      for (const auto & parameter : parameters) {
+        if (parameter.get_name() == "create_entities" &&
+          parameter.get_type() == rclcpp::ParameterType::PARAMETER_BOOL)
+        {
+          if (parameter.as_bool()) {
+            ASSERT_EQ(subscription_, nullptr);
+            rclcpp::SubscriptionOptions sub_options;
+            // This will declare the QoS override parameters in this callback.
+            sub_options.qos_overriding_options =
+              rclcpp::QosOverridingOptions::with_default_policies();
+            subscription_ = node->create_subscription<test_msgs::msg::Empty>(
+              "empty",
+              rclcpp::QoS(10),
+              sub_callback,
+              sub_options);
+            ASSERT_NE(subscription_, nullptr);
+            ASSERT_EQ(publisher_, nullptr);
+            rclcpp::PublisherOptions pub_options;
+            // This will declare the QoS override parameters in this callback.
+            pub_options.qos_overriding_options =
+              rclcpp::QosOverridingOptions::with_default_policies();
+            publisher_ = node->create_publisher<test_msgs::msg::Empty>(
+              "empty",
+              rclcpp::QoS(10),
+              pub_options);
+            ASSERT_NE(publisher_, nullptr);
+          } else {
+            ASSERT_NE(subscription_, nullptr);
+            subscription_.reset();
+            ASSERT_EQ(subscription_, nullptr);
+            ASSERT_NE(publisher_, nullptr);
+            publisher_.reset();
+            ASSERT_EQ(publisher_, nullptr);
+          }
+        }
+      }
+    };
+
+  auto handle = node_parameters->add_post_set_parameters_callback(callback);
+  ASSERT_NE(handle, nullptr);
+  EXPECT_TRUE(node_parameters->has_parameter("create_entities"));
+  EXPECT_EQ(node_parameters->get_parameter("create_entities").get_value<bool>(), false);
+
+  // This will call the registered callback, that will create endpoints with
+  // declaring the QoS override parameters recursively.
+  auto results = node_parameters->set_parameters({rclcpp::Parameter("create_entities", true)});
+  EXPECT_TRUE(!results.empty() && results[0].successful);
+
+  EXPECT_TRUE(node_parameters->has_parameter("create_entities"));
+  EXPECT_EQ(node_parameters->get_parameter("create_entities").get_value<bool>(), true);
+
+  // Destroy publisher and subscription endpoints.
+  results = node_parameters->set_parameters({rclcpp::Parameter("create_entities", false)});
+  EXPECT_TRUE(!results.empty() && results[0].successful);
+
+  EXPECT_TRUE(node_parameters->has_parameter("create_entities"));
+  EXPECT_EQ(node_parameters->get_parameter("create_entities").get_value<bool>(), false);
+
+  // Make sure recreation can also work without any exception.
+  results = node_parameters->set_parameters({rclcpp::Parameter("create_entities", true)});
+  EXPECT_TRUE(!results.empty() && results[0].successful);
+
+  EXPECT_NO_THROW(node_parameters->remove_post_set_parameters_callback(handle.get()));
+}
+
 TEST_F(TestNodeParameters, wildcard_with_namespace)
 {
   rclcpp::NodeOptions opts;


Reviews:
user: login: wjwwood
body: I'm still surprised that this is needed. I see that we explicitly tell people not to change parameters in the post change callback:

https://github.com/ros2/rclcpp/blob/605251bd71da0427ab2cdca26a9e737ca93e73d9/rclcpp/include/rclcpp/node.hpp#L1184

But I don't remember why. Do you have a good idea why this was the case @fujitatomoya? I really think we should understand this before we workaround it.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-18T17:02:20Z
user: login: fujitatomoya
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-24T21:33:35Z
user: login: jmachowinski
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-07T11:17:12Z
user: login: ahcorde
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T11:11:14Z
user: login: jmachowinski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T12:07:39Z
user: login: fujitatomoya
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T16:13:07Z
user: login: jmachowinski
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-09T08:38:34Z

Review comments:
diff_hunk: @@ -270,6 +270,12 @@ class NodeParametersInterface
   virtual
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const = 0;
+
+  /// Enable parameter modification recursively just once.
path: rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
user: login: wjwwood
body: How do you ensure the "just once" part?
created_at: 2025-02-18T16:48:27Z
author_association: MEMBER
diff_hunk: @@ -270,6 +270,12 @@ class NodeParametersInterface
   virtual
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const = 0;
+
+  /// Enable parameter modification recursively just once.
path: rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
user: login: fujitatomoya
body: next parameter update will set `parameter_modification_enabled_` to `false`, so no further operation will be allowed after that recursively. that is what i meant here by `just once`... i may be mistaken?
created_at: 2025-02-24T21:33:32Z
author_association: COLLABORATOR
diff_hunk: @@ -270,6 +270,12 @@ class NodeParametersInterface
   virtual
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const = 0;
+
+  /// Enable parameter modification recursively just once.
path: rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
user: login: jmachowinski
body: could you extend the documentation to reflect what you just wrote ?
created_at: 2025-04-07T11:16:08Z
author_association: COLLABORATOR
diff_hunk: @@ -214,6 +214,10 @@ class NodeParameters : public NodeParametersInterface
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const override;
 
+  RCLCPP_PUBLIC
path: rclcpp/include/rclcpp/node_interfaces/node_parameters.hpp
user: login: jmachowinski
body: documentation ?
created_at: 2025-04-07T11:16:47Z
author_association: COLLABORATOR
diff_hunk: @@ -270,6 +270,22 @@ class NodeParametersInterface
   virtual
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const = 0;
+
+  /// Enable parameter modification recursively during parameter callbacks.
+  /**
+   * This function is used to enable parameter modification during parameter operations.
+   *
+   * There are times when it does not allow parameter modification, such as when the parameter
+   * callbacks are being called and tries to modify the parameters with set_parameter and
+   * declare_parameter to avoid recursive parameter modification.
+   *
+   * This function is explicitly called to allow the recursive parameter operation during
+   * parameter callbacks by the application.
+   */
path: rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
user: login: jmachowinski
body: Does this somehow get disabled after some event ?
created_at: 2025-04-08T12:07:39Z
author_association: COLLABORATOR
diff_hunk: @@ -270,6 +270,22 @@ class NodeParametersInterface
   virtual
   const std::map<std::string, rclcpp::ParameterValue> &
   get_parameter_overrides() const = 0;
+
+  /// Enable parameter modification recursively during parameter callbacks.
+  /**
+   * This function is used to enable parameter modification during parameter operations.
+   *
+   * There are times when it does not allow parameter modification, such as when the parameter
+   * callbacks are being called and tries to modify the parameters with set_parameter and
+   * declare_parameter to avoid recursive parameter modification.
+   *
+   * This function is explicitly called to allow the recursive parameter operation during
+   * parameter callbacks by the application.
+   */
path: rclcpp/include/rclcpp/node_interfaces/node_parameters_interface.hpp
user: login: fujitatomoya
body: yeah, that is good question, and should have been added here, i will update the docstring.
created_at: 2025-04-08T16:13:07Z
author_association: COLLABORATOR

PR comments:
user: login: fujitatomoya
created_at: 2025-02-24T21:27:20Z
body: @wjwwood 

> But I don't remember why. Do you have a good idea why this was the case @fujitatomoya?

no i do not have any clue on this. i guess this has been here for a long time before i know that...
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-02-25T20:45:14Z
body: Summary:

- `add_pre_set_parameters_callback` and `add_on_set_parameters_callback` should never change (set/declare/undeclare) the parameter recursively because this could generates the parameter data base inconsistency. (e.g following callbacks might deny the parameter operation after the recursive parameter changes.)
- `add_post_set_parameters_callback` can change the parameter without parameter data base inconsistency explained above. but it could generate the infinite loop in the post set callbacks if the parameters are cross-referenced. to avoid this problem, it should avoid recursive parameter changes in default.
- we could have another thread to get this parameter operation done from the parameter callbacks in general use. but introducing another thread in the system would generate complication and racy condition. besides, operation could not be synchronized with callback completion.
- we could have the internal special APIs to work-around this particular problem when creating publisher and subscription with QoS override parameters possibly around `rclcpp::detail::declare_qos_parameters`. but it could be expected that during post set parameter callback, the application needs to change the parameter recursively, these special methods are not available for these general use cases.
- we can provide formalized API to explicitly allow the recursive parameter operation during parameter callback. (current PR proposal) in this case either system or user can control the parameter operation recursively, but they need to understand what they are doing once it is enabled. at least, API documentation should be more described about possible problems and consequences.
- Note: https://github.com/fujitatomoya/ros2_test_prover/blob/master/prover_rclcpp/src/create_sub_via_param.cpp is actually anti-pattern with using `add_on_set_parameters_callback`, it should be explained with `add_post_set_parameters_callback` instead.
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-04T05:45:37Z
body: @jmachowinski @alsora @mjcarroll any opinions? i would like to have this fix, so that we can have https://github.com/ros2/rclcpp/pull/2378
author_association: COLLABORATOR
user: login: alsora
created_at: 2025-04-04T14:13:59Z
body: Would it be possible to add some unit-tests to show that the changes work?
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-04T15:23:45Z
body: @alsora yeah, i will consider the test. thanks!
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-08T06:50:50Z
body: @alsora @jmachowinski test and docstring are added, can you take a look?
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-08T06:51:46Z
body: Pulls: ros2/rclcpp#2742
Gist: https://gist.githubusercontent.com/fujitatomoya/a54329825b20833084b9dff16cd9badf/raw/45adffa43e80fc56bd41ee9dc43cc8ded5beef7f/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/15628
* Linux [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux&build=23081)](http://ci.ros2.org/job/ci_linux/23081/)
* Linux-aarch64 [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-aarch64&build=17300)](http://ci.ros2.org/job/ci_linux-aarch64/17300/)
* Linux-rhel [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-rhel&build=2558)](http://ci.ros2.org/job/ci_linux-rhel/2558/)
* Windows [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_windows&build=23958)](http://ci.ros2.org/job/ci_windows/23958/)
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-08T18:12:26Z
body: @jmachowinski just added docstring a bit more, could you check again? thanks for the review!
author_association: COLLABORATOR
user: login: fujitatomoya
created_at: 2025-04-08T18:13:12Z
body: Pulls: ros2/rclcpp#2742
Gist: https://gist.githubusercontent.com/fujitatomoya/cbe2527f600e83296e014f4ceae60b40/raw/45adffa43e80fc56bd41ee9dc43cc8ded5beef7f/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/15634
* Linux [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux&build=23087)](http://ci.ros2.org/job/ci_linux/23087/)
* Linux-aarch64 [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-aarch64&build=17306)](http://ci.ros2.org/job/ci_linux-aarch64/17306/)
* Linux-rhel [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_linux-rhel&build=2564)](http://ci.ros2.org/job/ci_linux-rhel/2564/)
* Windows [![Build Status](http://ci.ros2.org/buildStatus/icon?job=ci_windows&build=23964)](http://ci.ros2.org/job/ci_windows/23964/)
author_association: COLLABORATOR
user: login: pele1410
created_at: 2025-07-08T11:46:43Z
body: Can this change be backported to Jazzy (or has it already been and just not released)?

I've got a PR in nav2 that needs this fix: https://github.com/ros-navigation/navigation2/pull/4961
author_association: NONE
user: login: jmachowinski
created_at: 2025-07-08T14:55:35Z
body: @pele1410 No, it is ABI breaking and can therefore not be directly back ported. 
You could perhaps create a non ABI breaking version of this by making enable_parameter_modification not part of the interface (and non virtual) and using an upcast in the declare_parameter_or_get function.
author_association: COLLABORATOR