Skip to content
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

Prevent API gaps between Node and LifecycleNode #898

Open
bpwilcox opened this issue Oct 22, 2019 · 13 comments
Open

Prevent API gaps between Node and LifecycleNode #898

bpwilcox opened this issue Oct 22, 2019 · 13 comments
Assignees
Labels
backlog enhancement New feature or request question Further information is requested

Comments

@bpwilcox
Copy link
Contributor

I was working on a feature using rclcpp_lifecycle::LifecycleNode to declare a parameter when I noticed that the declare_parameter API on the lifecycle node interface does not include the ignore_overrides flag as does the version in rclcpp::Node. It seems that in addition to this, there are a handful of functions that have yet to be implemented in the lifecycle node. From a brief comparison (I didn't check all the signatures so there could be other differences), I found these functions that are missing:

  • get_fully_qualified_name()
  • declare_parameter (missing ignore_overrides argument)
  • add_on_set_parameters_callback(OnParametersSetCallbackType callback);
  • remove_on_set_parameters_callback(const OnSetParametersCallbackHandle * const handler);
  • get_sub_namespace()
  • get_effective_namespace()
  • create_sub_node()
  • assert_liveliness()

I think some PRs to implement the above functions and close the gap on the common node API is a solution, but I also would propose a discussion on a common interface by which these nodes must abide. Apparently these APIs can go out of sync with new changes, so I'd like to hear thoughts on how to better synchronize these Node classes.

@wjwwood wjwwood added enhancement New feature or request question Further information is requested labels Nov 21, 2019
@fujitatomoya
Copy link
Collaborator

@bpwilcox

just idea, i think rclcpp_lifecycle::LifercycleNode is built on top of rclcpp::Node. so rclcpp_lifecycle::LifecycleNode Class inherits based on rclcpp::Node and featured with lifecycle interfaces. what i really care if for code maintenance. and also i cannot think of anything that is required for rclcpp::Node but not for rclcpp_lifecycle::LifercycleNode.

if i am not understanding correctly, could you enlighten me what discussion should be done here?

@bpwilcox
Copy link
Contributor Author

bpwilcox commented Nov 25, 2019

just idea, i think rclcpp_lifecycle::LifercycleNode is built on top of rclcpp::Node. so rclcpp_lifecycle::LifecycleNode Class inherits based on rclcpp::Node and featured with lifecycle interfaces.

Currently, the Node aand LifecycleNode classes are independent implementations, though both are built on top of the rclcpp::node_interfaces as members. For this reason, the APIs can go out of sync.

what i really care if for code maintenance. and also i cannot think of anything that is required for rclcpp::Node but not for rclcpp_lifecycle::LifercycleNode.

if i am not understanding correctly, could you enlighten me what discussion should be done here?

I agree, the LifecycleNode is primarily a superset of the Node interface, which is why I am opening discussion for whether we should have it inherit from the Node class or perhaps both inherit from a common base class, or other ideas.

@mjcarroll
Copy link
Member

I'm going to add a second issue to track just adding the currently missing APIs, and we can continue discussion on how to prevent this in the future on this issue.

@mjcarroll mjcarroll changed the title API gap between Node and LifecycleNode Prevent API gaps between Node and LifecycleNode Dec 5, 2019
@Barry-Xu-2018
Copy link
Collaborator

@bpwilcox @fujitatomoya @mjcarroll @Karsten1987

About the relationship of node and lifecycleNode, I think the design has been discussed in #318.
@Karsten1987 said "We decided to favor composition mainly of being able to test/mock the individual components of the node class. As you might have noticed, we tried to keep the basic interface / public member function of the lifecycle node as similar as the original node class. This is a) for not breaking the API too much and b) for being able to migrate the lifecycle components into the node class."

So the decision tends to move lifecycle into node class. I think Lifecycle can be as feature(component) of node and enabled by node option.

BTW, in order to avoid growing gap of API between libcycleNode and Node, moving should be done ASAP.

@fujitatomoya
Copy link
Collaborator

@Barry-Xu-2018

decision's been made before, but not sure we still get along with this.

@wjwwood could you share your thoughts if possible?

BTW, in order to avoid growing gap of API between libcycleNode and Node, moving should be done ASAP.

agree.

@wjwwood
Copy link
Member

wjwwood commented Mar 10, 2020

So the decision tends to move lifecycle into node class. I think Lifecycle can be as feature(component) of node and enabled by node option.

I'm not sure that would be a good idea. Perhaps it could be the right approach, but part of the interface of a lifecycle node is that you do not create normal publishers but lifecycle publishers instead. Doing what you suggest doesn't remove the normal create_publisher() method.

So, maybe something like this could work, but not without more thought and details as to how it would work. I don't have time to pursue that right now.

@fujitatomoya
Copy link
Collaborator

i do not have concrete design right now, not sure which way is suitable/reasonable at this time. I would come back on this, since i do not have resource either. or somebody else takes over the consideration.

@guru-florida
Copy link
Contributor

This may be far out but....what about deriving Node from LifecycleNode? IMO LifecycleNode is a beautiful implementation and should be defacto and pushed as best-practice. Node can just provide a default transition impl for on_configure|activate|deactivate|etc and also transition itself (unless a "self_manage" parameter is explicitly false or something). I think this fits the c++ best practices of "derived classes shouldnt modify behavior much" and in fact to the external observer there is no behavior change - only internal.

Benefits:

  • (Possibly) can be done with no changes to existing Node derived classes. Components derived from Node dont bother caring about lifecycle and existing Node-only based conponents shouldnt need any changes.
  • No API Gaps anymore
  • One single Node interface reduces ROS2 API complexity
  • Node writers can initially derive from Node, then later change to Lifecycle management without affecting their users (since they were always a Lifecycle node to users)

@hellantos
Copy link

hellantos commented Sep 25, 2022

I came across this problem when I tried to create a library that does provide both a LifecycleNode and rclcpp::Node. I have a class (lets call it DriverInterface) that should take LifecycleNode or rclcpp::Node in the constructor and add a bunch of services, subscriptions and publishers. In this class I would like to use the normal node functions such as create publisher, create client, create_timer, ... (not node_interface provided functions, as they are not well documented and require much reimplementation and maintenance if upstream changes). This seems to be impossible right now, even templating does not really work you, just end up reimplmenting most things seperately for both node types.

The current situation just makes handling Node and LifecycleNode instances with the same code a huge pain.
It would be super helpful if we had at least a NodeUserInterface with all common user functions - these are not in node_interfaces.

I would be willing to create and add node_user_interface.hpp and make LifecycleNode and Node inherit from it.

@clalancette
Copy link
Contributor

I would be willing to create and add node_user_interface.hpp and make LifecycleNode and Node inherit from it.

To be clear; it was an explicit design goal to not require inheritance between the two of them, to avoid the myriad problems with inheritance. Whether that is the right call is not totally clear, but we would definitely want to make sure we change/update the design if we were to go this route. That said...

This seems to be impossible right now, even templating does not really work you, just end up reimplmenting most things seperately for both node types.

This should be possible today, either by using templating (like what is done in https://github.com/ros2/message_filters/blob/rolling/include/message_filters/subscriber.h), or by using the Node interfaces instead of the node directly (like what is done in https://github.com/ros2/geometry2/blob/rolling/tf2_ros/include/tf2_ros/transform_listener.h#L119-L133).

@hellantos
Copy link

This should be possible today, either by using templating (like what is done in https://github.com/ros2/message_filters/blob/rolling/include/message_filters/subscriber.h), or by using the Node interfaces instead of the node directly (like what is done in https://github.com/ros2/geometry2/blob/rolling/tf2_ros/include/tf2_ros/transform_listener.h#L119-L133).

If the approach is to use the node interfaces, it would be super nice, if they would expose the standard functions of the node i.e. create_service, create_client, create_wall_timer etc. Which they partially do, but not completely.

@methylDragon
Copy link
Contributor

I think the difficulty in some of those (e.g. create_service) is that those methods require more than one node interface (which would mean implementing them as part of the node interfaces would result in overlaps between the interfaces.)

You'd call rclcpp::create_service(), passing in the node base interface and node services interfaces, I think.

@methylDragon
Copy link
Contributor

methylDragon commented Jan 27, 2023

Just an update,

There is now a third option aside from passing individual interfaces or templating on NodeT. (Though this only unifies any functionality that is defined within an interface, but not any of the node->XXX methods that aren't)

An aggregate class that implicitly converts node-likes into a collection of aggregated interfaces has been implemented in:

And can be used as such:

create_service(
   NodeInterfaces<NodeBaseInterface, NodeServicesInterface> node_handle,
   // ...

// User calls with (or can just pass in the (non pointer) object for implicit conversion))
auto service = create_service(
  NodeInterfaces<NodeBaseInterface, NodeServicesInterface>(*my_node_or_lifecycle_node_class_ptr),
  // ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backlog enhancement New feature or request question Further information is requested
Projects
None yet
Development

No branches or pull requests

9 participants