-
Notifications
You must be signed in to change notification settings - Fork 528
Filter service requests for inactive managed nodes #1836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: rolling
Are you sure you want to change the base?
Changes from all commits
b8eda0c
49a2685
5a930ec
a2f938c
bba7c9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,7 @@ | |
|
|
||
| #include "rclcpp_lifecycle/node_interfaces/lifecycle_node_interface.hpp" | ||
| #include "rclcpp_lifecycle/lifecycle_publisher.hpp" | ||
| #include "rclcpp_lifecycle/lifecycle_service.hpp" | ||
| #include "rclcpp_lifecycle/state.hpp" | ||
| #include "rclcpp_lifecycle/transition.hpp" | ||
| #include "rclcpp_lifecycle/visibility_control.h" | ||
|
|
@@ -268,10 +269,14 @@ class LifecycleNode : public node_interfaces::LifecycleNodeInterface, | |
|
|
||
| /// Create and return a Service. | ||
| /** | ||
| * \sa rclcpp::Node::create_service | ||
| * \param[in] service_name The topic to service on. | ||
| * \param[in] callback User-defined callback function. | ||
| * \param[in] qos_profile rmw_qos_profile_t Quality of service profile for client. | ||
| * \param[in] group Callback group to call the service. | ||
| * \return Shared pointer to the created lifecycle service. | ||
| */ | ||
| template<typename ServiceT, typename CallbackT> | ||
| typename rclcpp::Service<ServiceT>::SharedPtr | ||
| typename rclcpp_lifecycle::LifecycleService<ServiceT>::SharedPtr | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is the same we do for publishers, but IMO it would be better to have You can currently still create a non-managed entity by using the full method path, but having non-overriding methods would be clearer IMO. This would be a breaking change, so we can ignore it for the moment.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. Consider some code that operates on a node-like object. It nice to be able to pass in an However, I'm not sure if this idea of passing Node and LifecycleNode interchangeable is true in practice. If a user chooses LifecycleNode, then I would think they want managed entities in most cases. So, I lean towards "overriding" I guess either way, we'll be breaking API (currently this PR is changing the return type). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coming back to this a bit later being interested in the suite of managed entity implementation I would agree with @ivanpauno thinking from a potential user bug perspective. I could imagine a user who wants a mixture of entity types with some being active even in This may* be poor user design in theory but I could easily see someone trying to do this. If @jacobperron I think you bring up a great point, I'm not sure the feature parity between the two at the moment Thinking out loud: I can see the other side, however, where you likely want to use |
||
| create_service( | ||
| const std::string & service_name, | ||
| CallbackT && callback, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |||||||||||||||||||||
| #include "rclcpp/type_support_decl.hpp" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| #include "lifecycle_publisher.hpp" | ||||||||||||||||||||||
| #include "lifecycle_service.hpp" | ||||||||||||||||||||||
| #include "rclcpp_lifecycle/visibility_control.h" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| #include "rclcpp_lifecycle/lifecycle_node.hpp" | ||||||||||||||||||||||
|
|
@@ -119,16 +120,24 @@ LifecycleNode::create_client( | |||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| template<typename ServiceT, typename CallbackT> | ||||||||||||||||||||||
| typename rclcpp::Service<ServiceT>::SharedPtr | ||||||||||||||||||||||
| typename rclcpp_lifecycle::LifecycleService<ServiceT>::SharedPtr | ||||||||||||||||||||||
| LifecycleNode::create_service( | ||||||||||||||||||||||
| const std::string & service_name, | ||||||||||||||||||||||
| CallbackT && callback, | ||||||||||||||||||||||
| const rmw_qos_profile_t & qos_profile, | ||||||||||||||||||||||
| rclcpp::CallbackGroup::SharedPtr group) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| return rclcpp::create_service<ServiceT, CallbackT>( | ||||||||||||||||||||||
| node_base_, node_services_, | ||||||||||||||||||||||
| service_name, std::forward<CallbackT>(callback), qos_profile, group); | ||||||||||||||||||||||
| rclcpp::AnyServiceCallback<ServiceT> any_service_callback; | ||||||||||||||||||||||
| any_service_callback.set(std::forward<CallbackT>(callback)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| rcl_service_options_t service_options = rcl_service_get_default_options(); | ||||||||||||||||||||||
| service_options.qos = qos_profile; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| auto service = LifecycleService<ServiceT>::make_shared( | ||||||||||||||||||||||
| node_base_->get_shared_rcl_node_handle(), service_name, any_service_callback, service_options); | ||||||||||||||||||||||
| auto serv_base_ptr = std::dynamic_pointer_cast<rclcpp::ServiceBase>(service); | ||||||||||||||||||||||
| node_services_->add_service(serv_base_ptr, group); | ||||||||||||||||||||||
| return service; | ||||||||||||||||||||||
|
fujitatomoya marked this conversation as resolved.
Comment on lines
+130
to
+140
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to replace this code with something similar to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered it, and I did not do it because I thought I had to change the create_service template of rclcpp, which is somewhat far from the purpose of this PR. Unlike rclcpp/rclcpp/include/rclcpp/create_publisher.hpp Lines 45 to 46 in 7a2ee23
rclcpp/rclcpp/include/rclcpp/create_service.hpp Lines 33 to 34 in 7a2ee23
In the next PR, it is planned to change the create_service part to a factory design like a publisher. https://github.com/ros2/rclcpp/pull/1836/files#r767374194 |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| template<typename AllocatorT> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| // Copyright 2021 Open Source Robotics Foundation, Inc. | ||
| // | ||
| // 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. | ||
|
|
||
| #ifndef RCLCPP_LIFECYCLE__LIFECYCLE_SERVICE_HPP_ | ||
| #define RCLCPP_LIFECYCLE__LIFECYCLE_SERVICE_HPP_ | ||
|
|
||
| #include <memory> | ||
| #include <string> | ||
|
|
||
| #include "rclcpp/any_service_callback.hpp" | ||
| #include "rclcpp/service.hpp" | ||
|
|
||
| namespace rclcpp_lifecycle | ||
| { | ||
| /// base class with only | ||
| /** | ||
| * pure virtual functions. A managed | ||
| * node can then deactivate or activate | ||
| * the service handling. | ||
| * It is more a convenient interface class | ||
| * than a necessary base class. | ||
| */ | ||
| class LifecycleServiceInterface | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there any reason why we need a new interface and this cannot be the same as
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO, using LifecyclePublisherInterface in a service seemed odd. I thought of unified interface ( By the way, if #1846 is accepted, there may be no compatibility issues as that API will become an internal API. |
||
| { | ||
| public: | ||
| virtual ~LifecycleServiceInterface() {} | ||
| virtual void on_activate() = 0; | ||
| virtual void on_deactivate() = 0; | ||
| virtual bool is_activated() = 0; | ||
| }; | ||
|
|
||
| /// brief child class of rclcpp Service class. | ||
| /** | ||
| * Overrides all service functions to check for enabled/disabled state. | ||
| */ | ||
| template<typename ServiceT> | ||
| class LifecycleService : public LifecycleServiceInterface, public rclcpp::Service<ServiceT> | ||
| { | ||
| public: | ||
| RCLCPP_SMART_PTR_DEFINITIONS(LifecycleService) | ||
|
|
||
| using rclcpp::Service<ServiceT>::Service; | ||
|
|
||
| virtual ~LifecycleService() {} | ||
|
|
||
| /// add to wait set | ||
| /** | ||
| * The function checks whether the communication | ||
| * was enabled or disabled and forwards the add_to_wait_set | ||
| * request to the actual rclcpp Service base class | ||
| */ | ||
| bool add_to_wait_set(rcl_wait_set_t * wait_set) override | ||
| { | ||
| if (!enabled_) { | ||
| // The memory strategy only expects false in case of "failure", | ||
| // so it returns true even if no service is added. | ||
| return true; | ||
|
huchijwk marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return rclcpp::Service<ServiceT>::add_to_wait_set(wait_set); | ||
| } | ||
|
|
||
| void on_activate() override {enabled_ = true;} | ||
|
|
||
| void on_deactivate() override {enabled_ = false;} | ||
|
|
||
| bool is_activated() override {return enabled_;} | ||
|
Comment on lines
+74
to
+78
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also maybe add a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will implement it together with the additional PR related to the ManagedEntityInterface mentioned above. how is it? My PR plan is as follows.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct me if I'm mistaken, but can we just rename this interface to |
||
|
|
||
| private: | ||
| bool enabled_ = false; | ||
| }; | ||
|
|
||
| } // namespace rclcpp_lifecycle | ||
|
|
||
| #endif // RCLCPP_LIFECYCLE__LIFECYCLE_SERVICE_HPP_ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| // Copyright 2021 Open Source Robotics Foundation, Inc. | ||
| // | ||
| // 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. | ||
|
|
||
| #include <gmock/gmock.h> | ||
| #include <gtest/gtest.h> | ||
|
|
||
| #include <chrono> | ||
| #include <memory> | ||
| #include <string> | ||
|
|
||
| #include "rclcpp/rclcpp.hpp" | ||
| #include "rclcpp_lifecycle/lifecycle_node.hpp" | ||
| #include "rclcpp_lifecycle/lifecycle_service.hpp" | ||
| #include "test_msgs/srv/empty.hpp" | ||
|
|
||
| using namespace std::literals::chrono_literals; | ||
|
|
||
| class TestLifecycleService : public ::testing::Test | ||
| { | ||
| public: | ||
| void SetUp() | ||
| { | ||
| rclcpp::init(0, nullptr); | ||
| node_ = rclcpp_lifecycle::LifecycleNode::make_shared("node"); | ||
| service_ = node_->create_service<test_msgs::srv::Empty>( | ||
| std::string("service"), request_callback_.AsStdFunction()); | ||
| client_ = node_->create_client<test_msgs::srv::Empty>(std::string("service")); | ||
| } | ||
|
|
||
| void TearDown() {rclcpp::shutdown();} | ||
|
|
||
| protected: | ||
| rclcpp_lifecycle::LifecycleNode::SharedPtr node_; | ||
| rclcpp_lifecycle::LifecycleService<test_msgs::srv::Empty>::SharedPtr service_; | ||
| rclcpp::Client<test_msgs::srv::Empty>::SharedPtr client_; | ||
|
|
||
| ::testing::MockFunction<void( | ||
| const test_msgs::srv::Empty::Request::SharedPtr, test_msgs::srv::Empty::Response::SharedPtr)> | ||
| request_callback_; | ||
| }; | ||
|
|
||
| TEST_F(TestLifecycleService, request_on_active_service) | ||
| { | ||
| service_->on_activate(); | ||
| EXPECT_TRUE(service_->is_activated()); | ||
|
|
||
| ::testing::MockFunction<void(std::shared_future<std::shared_ptr<test_msgs::srv::Empty_Response>>)> | ||
| response_callback; | ||
|
|
||
| auto request = std::make_shared<test_msgs::srv::Empty::Request>(); | ||
| client_->wait_for_service(); | ||
| auto future = client_->async_send_request(request, response_callback.AsStdFunction()); | ||
|
|
||
| EXPECT_CALL(request_callback_, Call(::testing::_, ::testing::_)).WillOnce(::testing::Return()); | ||
| EXPECT_CALL(response_callback, Call(::testing::_)).WillOnce(::testing::Return()); | ||
|
|
||
| rclcpp::spin_until_future_complete(node_->get_node_base_interface(), future, 1s); | ||
| } | ||
|
|
||
| TEST_F(TestLifecycleService, request_on_inactive_service) | ||
| { | ||
| service_->on_deactivate(); | ||
| EXPECT_FALSE(service_->is_activated()); | ||
|
|
||
| ::testing::MockFunction<void(std::shared_future<std::shared_ptr<test_msgs::srv::Empty_Response>>)> | ||
| response_callback; | ||
|
|
||
| auto request = std::make_shared<test_msgs::srv::Empty::Request>(); | ||
| client_->wait_for_service(); | ||
|
huchijwk marked this conversation as resolved.
|
||
| auto future = client_->async_send_request(request, response_callback.AsStdFunction()); | ||
|
|
||
| EXPECT_CALL(request_callback_, Call(::testing::_, ::testing::_)).Times(0); | ||
| EXPECT_CALL(response_callback, Call(::testing::_)).Times(0); | ||
|
|
||
| rclcpp::spin_until_future_complete(node_->get_node_base_interface(), future, 1s); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My one concern with this new API is how it will interact with the existing
WaitSetclasses. I suppose this will not work as expected if someone chooses to use aWaitSetobject. Maybe we should update this statement to use the new API:rclcpp/rclcpp/include/rclcpp/wait_set_policies/detail/storage_policy_common.hpp
Lines 362 to 365 in 2d6e636
I'm not sure though; I'd like to hear from @ivanpauno and @wjwwood
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this new method seems like a poor way to control whether or not a service is handled in the managed node, and it won't work with the WaitSet classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we don't have lifecycle subscriptions yet either, but I would say we need a new way to indicate whether a subscription should be waited on or not.
Initially I thought the callback groups could already be used for this, but you could have a callback group with one normal service and one lifecycle service. So I don't think that will be useful.
The other question is, should it just not "get execution time" as was quoted in the first comment of this pr, or should requests received while not active be discarded? I.e., imagine: "service is inactive" -> "request is received" -> "service becomes active", should the request received before being active be handled? My intuition is no, but I'm not 100% sure.
If we said that requests received during an inactive state are ignored, we should implement the lifecycle service using a callback that wraps the user's callback and only calls it if the lifecycle state is active.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wjwwood Hmm... ignoring requests in inactive state seems fine too. If we decide to ignore requests, what do you think about filtering by overriding the handle request instead of wrapping the callback? (The initial implementation did that.)
@alsora what do you think about ignoring requests in inactive state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the design document https://design.ros2.org/articles/node_lifecycle.html
This seems to only refer to execution time, i.e. the executor will ignore the entity for as long as it is inactive.
From a performance point of view, the current approach is also much more efficient than having a "wrapper callback".
The "wrapper callback" approach requires deserialization, going through all the ROS layers and doing work in the executor.
When a server is inactive, it may be for a variety of reason, but I wouldn't want it to perform any work at all.
Lastly, although a wrapper callback may work with subscriptions, how would that be implemented with servers where the callback usually needs to send a response back to the client?
Will this require for the response to have a "success/failure" bool field ? How does the client interpret if the server invocation failed due to the server being inactive or due to problems in the request?
For what concerns the fact that this won't work with the new WaitSet class, is this really a problem? The point of the WaitSet class is to decouple waiting and scheduling. The user is responsible for taking the data and executing it, so it should be easy to handle this.
Anyhow, can you elaborate why this wouldn't work? The WaitSet class is currently calling
rcl_wait_set_add_service, can't we have it callserver->add_to_wait_set()API which is overridden here?For what is worth, I looked into how to implement lifecycle entities also with the RMW listener APIs and the events executor. Here the approach would be slightly different since the
add_to_wait_setfunction is not invoked.The approach would consist in the
on_activateandon_deactivatefunctions to remove/restore the listener callback if present.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That could work, but maybe it produces overhead you don't want, as @alsora said.
I think to achieve this we would need to have a new active state in each entity and the executor/wait set in rclcpp would need to know to ignore it when it is inactive, as well we'd need a way to notify the executor/wait set when that state changes (so it can build a new wait set that includes it).
Once we have that, we can either let the messages/requests accumulate or we can clear them as we become active, which ever seems appropriate (we could even have a configuration for that).
I would say the request is taken (cleared out), but never acted on. This would be no different than any other timed out service call from the client's perspective. This is already the case and a shortcoming of services, so it's something worth improving, but not specific to this issue (lifecycle behavior), as there are other reasons a service may appear available to the client but fail to respond (service callback throws or service server is destroyed before request is received, etc).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can work, the
WaitSetclass just needs to be updated. However, I would not do it by overriding theadd_to_wait_set()method. I would instead add a state to all waitable entities like "active" and let the wait set introspect that when deciding whether or not to add it to the rcl wait set.Plus as I said before, you need a feedback mechanism so that when that state changes the wait set can wake up and rebuild the wait set to include the newly active items.