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

Composition API #599

Open
wants to merge 19 commits into
base: rolling
Choose a base branch
from
Open

Conversation

crystaldust
Copy link
Contributor

@crystaldust crystaldust commented Jul 14, 2020

The composition API for rclpy.
Add a new rclpy_components sub project just like what rclcpp_components does. The project provides the component manager(essentially a wrapper of Node and executor), which provides the required services described in the composition design doc for the ros2cli to load/unload/list components

Related PR: ros2/ros2cli#554

@crystaldust
Copy link
Contributor Author

crystaldust commented Jul 14, 2020

Missing features/TODO list records:

  • The supported_types service(discussed here), need a discussion on where to put this service, put it in the composition_interfaces package, or just the rclpy package.
  • Need some essential test cases

Copy link
Member

@jacobperron jacobperron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good start! I've left some preliminary feedback. I'll take a look at the connected ros2cli PR and examples next.

self.components = {} # key: unique_id, value: full node name and component instance
self.unique_id_index = 0

self.executor.spin()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we're also calling spin in the containers, which is a blocking call, I'm surprised that the advertised services work (I haven't tried it myself yet). I would think that we don't need to call spin here.

<license>TODO: License declaration</license>

<depend>rclpy</depend>
<depend>std_msgs</depend>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like we're not depending on std_msgs, but instead we need composition_interfaces.


def main():
try:
_main()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than wrapping all of the logic in _main() in a try-catch, I think we can narrow the scope to the spin() call and remove the redundant "main" function. For example, see how it looks in this demo.

from importlib_metadata import entry_points

RCLPY_COMPONENTS = 'rclpy_components'
logger = get_logger('ComponentManager')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a global logger, why not just use the nodes logger? For example.

zip_safe=True,
maintainer='root',
maintainer_email='root@todo.todo',
description='TODO: Package description',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please populate the description.

<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="root@todo.todo">root</maintainer>
<license>TODO: License declaration</license>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<license>TODO: License declaration</license>
<license>Apache License 2.0</license>

<name>rclpy_components</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="root@todo.todo">root</maintainer>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<maintainer email="root@todo.todo">root</maintainer>
<maintainer email="jacob@openrobotics.org">Jacob Perron</maintainer>

<package format="3">
<name>rclpy_components</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please populate this description.

<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>rclpy_components</name>
<version>0.0.0</version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For maintenance reasons, this should match the version of other packages in this repository.

Suggested change
<version>0.0.0</version>
<version>1.1.0</version>

@crystaldust
Copy link
Contributor Author

Thanks for the review! Just get myself back to work after a LONG business travel. I'll look into the code and make some fixes/improvments

@crystaldust
Copy link
Contributor Author

crystaldust commented Oct 19, 2020

@jacobperron Hi, I've made some changes following the reviews, make a list here for better track:

  • Remove the unnecessary spin in ComponentManager's __init__ method
  • Make up the meta data in package.xml and setup.py, like author info, desc, version, license etc
  • Surround spin with try-catch, instead of creating a shadow _main function
  • Call component manager's destroy node when exiting program
  • Use the node's logger instead of creating a new global one
  • Use f string for logging
  • Return error message in response instead of logging it
  • Use request.id(type integer) as ComponentManager's components member as key

Also with the basic unit/integrated test code added. Let's first make this PR merged and then move to next PRs related to ros2cli and examples

Copy link
Contributor

@sloretz sloretz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great start! I like the use of entry points for finding components. Any idea why the multithreaded test is failing?

<name>rclpy_components</name>
<version>1.1.0</version>
<description>The dynamic node management package</description>
<maintainer email="juzhen@huawei.com">Zhen Ju</maintainer>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding yourself as an <author> tag, and adding @claireyywang @ivanpauno and myself as maintainers?

rclpy/rclpy/package.xml

Lines 8 to 10 in 22816b4

<maintainer email="clairewang@openrobotics.org">Claire Wang</maintainer>
<maintainer email="ivanpauno@ekumenlabs.com">Ivan Paunovic</maintainer>
<maintainer email="sloretz@openrobotics.org">Shane Loretz</maintainer>

executor.spin()
except KeyboardInterrupt:
print('KeyboardInterrupt received, exit')
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - no need for pass here :)

assert mock_res.error_message
assert (not mock_res.success)

# Unload the first node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might become the only test in rclpy that depends on the state of the previous tests. I've never seen this style before. IIUC it works because the test runner sorts tests alphabetically.

No need to change anything here - I'm fine with this as is.

unload_res: UnloadNode.Response = self.__class__.unload_node(1000)
assert unload_res.error_message != ""
assert unload_res.success is False
list_res: ListNodes.Response = self.__class__.list_nodes()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - don't need to specify __class__ when calling class methods; self.list_nodes() works


def list_nodes_test(self):
container_name = self.__class__.container_name
print(f'{container_name}: list_nodes tested within test_load_node and test_unload_node')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, I would delete this function since it doesn't test anything.

self.list_nodes_test()


class TestComponentManagerMT(TestComponentManager):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a test failure here when I merged this branch into master locally. Any ideay why?

============================= test session starts ==============================
platform linux -- Python 3.8.2, pytest-4.6.9, py-1.8.1, pluggy-0.13.0
cachedir: /home/sloretz/bigssd/ros2/build/rclpy_components/.pytest_cache
rootdir: /home/sloretz/bigssd/ros2/src/ros2/rclpy, inifile: pytest.ini
plugins: ament-copyright-0.10.0, ament-pep257-0.10.0, ament-flake8-0.10.0, ament-lint-0.10.0, colcon-core-0.6.0, cov-2.8.1, mock-1.10.4
collecting ...                                 
collected 13 items                                                             

test/test_component_manager.py .F                                        [ 15%]
test/test_component_manager_ut.py ........                               [ 76%]
test/test_copyright.py .                                                 [ 84%]
test/test_flake8.py .                                                    [ 92%]
test/test_pep257.py .                                                    [100%]

=================================== FAILURES ===================================
_________________ TestComponentManagerMT.test_composition_api __________________
test/test_component_manager.py:187: in test_composition_api
    self.load_node_test()
test/test_component_manager.py:130: in load_node_test
    load_res = self.__class__.load_node(TEST_COMPOSITION, TEST_COMPOSITION_FOO)
test/test_component_manager.py:93: in load_node
    raise RuntimeError(f'No load service found in /{cls.container_name}')
E   RuntimeError: No load service found in /TestComponentManagerMT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember to add some components specifically for the test, so the test won't have to rely on any pre-existing components. It seems the testing components are not installed, I'll take a look into this.

@jacobperron jacobperron removed their assignment Feb 26, 2021
@buschbapti
Copy link

Any update on this? We have the need for launching python components and having that integrated would be very neat.

@crystaldust
Copy link
Contributor Author

@buschbapti Thanks for the attention. I'll make a recheck on this and see if it can be merged ASAP.

@buschbapti
Copy link

I copied your rclpy_components to make this work in our application. Everything runs smoothly but I needed to change the get_fully_qualified_node_name line. Either this function has been deleted in the latest version (galactic) or is not integrated yet.

@alpylmz
Copy link

alpylmz commented Apr 29, 2022

This seems to be an important feature and it is almost done! Any update on this?

@SteveMacenski
Copy link

+1

@ihasdapie
Copy link
Member

@crystaldust Are you still working on this? If not I'd be take over and help push this PR through.

@audrow audrow changed the base branch from master to rolling June 28, 2022 14:21
@MathieuxHugo
Copy link

Hello any updates on this? Will it be integrated?

@domire8
Copy link

domire8 commented Feb 16, 2024

@mjcarroll @sloretz @crystaldust just trying to bump this again, we've been using it for 2 years and works as expected

The project provide component container(special Node) and
component_manager

Signed-off-by: Zhen Ju <juzhen@huawei.com>
Signed-off-by: Zhen Ju <juzhen@huawei.com>
This is to get rid of the impact of global remapping rules on node
name, which will create nodes with duplicated names.

Remove *args in ComponentManager's constructor.

Signed-off-by: Zhen Ju <juzhen@huawei.com>
* Update the meta info in package.xml and setup.py
* Try-except executor.spin instead of main in executables
* Use f string to format infos
* Pass error message to response objectc instead of logging it
* Use node's logger instead of init a global one
* Call node.get_qualified_name() to assign response obj's full_node_name
* Other super tiny changes

Signed-off-by: Zhen Ju <juzhen@huawei.com>
Signed-off-by: Zhen Ju <juzhen@huawei.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
Signed-off-by: Shane Loretz <shane.loretz@gmail.com>
@sloretz
Copy link
Contributor

sloretz commented Mar 31, 2024

Since it had been a while I rebased this onto rolling and made some changes

  • Resolved all flake8 linter issues
  • Updated setup.py and package.xml to list Zhen Ju as the author with @adityapande-1995 and I as the maintainers
  • Rewrote tests to use pytest and made each test independent. Previously the tests were dependent and relied on the tests being executed in alphabetical order, but that must have changed (or one of my installed pytest plugins changed it) because the tests weren't passing on my machine.
  • Fixed code that assumed .keys() and .values() from a dict would always be the same order to use .items()
  • Made the component_manager use the LoadNode.package_name parameter. The entrypoint specification is still package_name::PluginName, but now the component manager looks for an entry point named that way instead of using only plugin_name.
  • Removed tests_components package marker file and put the test node into the rclpy_components.test subpackage
  • Renamed the test node and file so that pytest didn't think it was a test
  • Added a check to make sure the fully qualified name of the loaded node is unique in the component manager to satisfy requirement in this design doc
  • Protected component_manager from crashing in case the entrypoint or class itself fail to load or be instantiated
  • Made node name and namespace get passed in via remap arguments instead of direct arguments to match rclcpp. The remap rules get evaluated with a different priority from the passed in node name so this makes that behavior consistent between the two
  • Added more tests (failure handling cases + making sure name and namespace remapping worked)
  • Use non-deprecated entry_points API
  • Removed importlib_metata as we don't need to support Python less than 3.8 on rolling
  • Use non-deprecated setup.cfg types.

And with that, this PR LGTM, but I've made so many changes I think it needs another reviewer.

Copy link
Contributor

@sloretz sloretz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with green CI and a second reviewer!

@sloretz
Copy link
Contributor

sloretz commented Mar 31, 2024

CI (repos file build: --packages-up-to rclpy_components test: --packages-select rclpy_components)

  • Linux Build Status
  • Linux-aarch64 Build Status
  • Windows Build Status

EDIT: the tests on Windows got stuck. I don't have access to a Windows machine at the moment to debug this :(

Copy link
Member

@wjwwood wjwwood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks reasonable to me, and since it's a new package then it comes with little enough risk in my opinion. I'd say if you @sloretz and @adityapande-1995 are ok with it (since you're the new maintainers of the new package), then lgtm.

Are there any examples?

@alberthli
Copy link

Any chance this could get merged soon? I'd also be interested in a backport at least through Humble.

@hello-amal
Copy link

Ditto to the value of a humble backport soon. Thanks all for your hard work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet