-
Notifications
You must be signed in to change notification settings - Fork 227
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
Implement node parameters and parameter services #214
Conversation
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.
a couple questions on the expected behavior before diving into actual code review
rclpy/rclpy/parameter.py
Outdated
return cls(rcl_param.name, type_, value) | ||
|
||
def __init__(self, name, type_, value): | ||
assert isinstance(type_, Parameter.Type) |
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.
should we raise a TypeError exception rather than asserting ?
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 wasn't certain if TypeError
s were the correct exception and there were other assert isinstance(...
instances throughout. I'd be inclined to flip these to TypeErrors if that's the usual pattern.
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.
per today's offline discussion we'll move type assertions to raise TypeErrors in this PR and I'll try to send a followup PR migrating assertions to type errors in the existing code that I used as an example.
rclpy/rclpy/parameter.py
Outdated
def get_descriptor(self): | ||
return ParameterDescriptor(name=self.name, type=self.type.value) | ||
|
||
def get_parameter_value(self): |
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.
What happens if the parameter is NOT_SET ? it looks like I could do:
set_parameters([Parameter('not_set_param', Parameter.Type.NOT_SET, 42)])
get_parameter('not_set_param').value
And successfully retrieve 42. Is this expected?
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.
If per your later question we end up enforcing parity between declared type and given value that closes this odd behavior.
I ended up creating a positional args constructor for Parameter but maybe I should change it to be a kwarg constructor to make it possible to omit the value for a 'not set' parameter. Deleting parameters isn't currently part of the rclcpp parameter API but we could make it part of the rclpy API if we want.
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.
per today's offline discussion we'll add code to parameter initialization to prevent setting values that don't match declared type (including setting any value for a parameter of type NOT_SET.
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.
This line of review is resolved by adding creation-time type checking in 39698fb.
rclpy/rclpy/parameter.py
Outdated
def get_parameter_value(self): | ||
parameter_value = ParameterValue(type=self.type.value) | ||
if Parameter.Type.BOOL == self.type: | ||
parameter_value.bool_value = self.value |
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.
should we enforce types here (and below) ? it looks like is can do the following:
self.node.set_parameters([Parameter('not_set_param', Parameter.Type.BOOL, 'foo')])
self.assertIsInstance(self.node.get_parameter('not_set_param'), Parameter)
self.assertEqual(self.node.get_parameter('not_set_param').value, 'foo')
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 think it's possible to enforce types on the Parameters. An earlier implementation of the Parameter class determined the type from the value given to the constructor but when I added the Parameter.Type enum to wrap the interface enum values I dropped all of that.
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.
per today's offline discussion we'll add code to parameter initialization to prevent setting values that don't match declared type (including setting any value for a parameter of type NOT_SET.
rclpy/rclpy/parameter_service.py
Outdated
nodename = node.get_name() | ||
|
||
describe_parameters_service_name = '/'.join((nodename, 'describe_parameters')) | ||
node.create_service( |
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 think we should use the parameter qos profile by default for all these services (I believe that would match rclcpp's implementation):
Line 179 in 98cdade
qos_profile_parameters = _rclpy.rclpy_get_rmw_qos_profile( |
And the parameter event publisher should use the corresponding profile as well (but I think this will be in a follow-up PR)
This looks great ! I didn't get a chance to thoroughly review it yet so just dropped a note for the questions I had. I'll give a review tomorrow morning if noone gets to it by then |
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.
the overall logic looks good to me 👍
I added a few comments for what's not covered by previous review.
2 questions:
- Do you have a set of prefixed parameters used for testing ?
- Where will the testing of the callbacks happen ? in a separate PR here ? or just in a higher level integration test?
rclpy/rclpy/parameter_service.py
Outdated
if not 0 == request.depth: | ||
# A depth of zero indicates infinite depth | ||
names_with_prefixes = filter( | ||
lambda name: name.count('.') + 1 <= request.depth, names_with_prefixes |
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.
Nit: name.count('.') < request.depth
rclpy/rclpy/parameter_service.py
Outdated
def _list_parameters_callback(self, request, response): | ||
names_with_prefixes = [] | ||
for name in self._node._parameters.keys(): | ||
if '.' in name: |
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.
'.'
should be made a constant and reused everywhere in the code so that we need to change only one place if we ever use a different separator (related to ros2/ros2#402)
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 looks like this comment is still pending (same for other uses of '.'
below)
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.
Whoops, thought I got all of them. Addressed by 67436b0
rclpy/rclpy/parameter_service.py
Outdated
return response | ||
|
||
if not 0 == request.depth: | ||
# A depth of zero indicates infinite depth |
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.
Does this refer to DEPTH_RECURSIVE
?
If yes we should use that constant
rclpy/rclpy/parameter_service.py
Outdated
if request.prefixes: | ||
for prefix in request.prefixes: | ||
prefix_with_trailing_dot = '%s.' % (prefix) | ||
if name.startswith(prefix_with_trailing_dot): |
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.
Nit:
if name.startswith(prefix + PARAMETER_SEPARATOR_STRING)
response.types.append(self._node.get_parameter(name).get_parameter_type()) | ||
return response | ||
|
||
def _list_parameters_callback(self, request, response): |
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.
Question: do you have examples of parameters that prompted the logic in this function ?
It looks to me like it could be simplified but I'd like to make sure I don't miss some test cases you used for development.
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.
See #214 (comment) for test cases used.
During our sync meeting we talked about unit tests going here and integration tests happening in system tests (I haven't written any there yet either) but I guess there's nothing stopping me from unit testing the callback functions. |
Good point, I remember that now. |
rclpy/rclpy/parameter_service.py
Outdated
prefix = '.'.join(name.split('.')[0:-1]) | ||
if prefix not in response.result.prefixes: | ||
response.result.prefixes.append(prefix) | ||
response.result.names.append(name) |
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.
The definition of the result message is ambiguous IMO: https://github.com/ros2/rcl_interfaces/blob/f00fe22a36c8fdf6145d873173a4f9de0fdff131/rcl_interfaces/msg/ListParametersResult.msg.
It's unclear to me if the name of the "parameters under the prefix" should be:
- the full parameter names (of the parameters that have a prefix matching one of the requested ones)
- parameter names stripped of the prefix (of the parameters that have a prefix matching one of the requested ones)
The current implementation seems to match the implementation of rclcpp so I think it's good 👍. But maybe it would be worth rephrasing or addressing the todo in the message definition. Or clarify what we expect the prefixes
field in the response to be used for.
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.
maybe @wjwwood has more context regarding the use of the prefixes
field in the result message?
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 don't know for sure, but my initial feeling is that returning the absolute names and prefixes would be best.
maybe @wjwwood has more context regarding the use of the
prefixes
field in the result message?
The prefixes
field contains additional nested prefixes as opposed to actual parameters. You can think of them as the "directories" when doing ls
in a directory, as opposed to the actual files in the directory.
a356f77
to
4cd964a
Compare
@mikaelarguedas this is ready for another round of review. |
rclpy/rclpy/parameter.py
Outdated
return self._name | ||
|
||
@property # noqa: A003 | ||
def type(self): |
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.
not sure what to do about this, it feels like we should avoid using builtin names here.
@ros2/team how do you feel about using builtin names as method names ?
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 think a property is similar enough to a public attribute and a builtin is similar enough to a reserved keyword that a trailing underscore would be appropriate
https://legacy.python.org/dev/peps/pep-0008/#names-to-avoid
If your public attribute name collides with a reserved keyword, append a single trailing underscore to your attribute name. This is preferable to an abbreviation or corrupted spelling.
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.
We could lengthen the field name to parameter_type
. It's redundant and burns a few extra milliseconds typing but avoids the ambiguity.
rclpy/rclpy/parameter_service.py
Outdated
self._node = node | ||
nodename = node.get_name() | ||
|
||
describe_parameters_service_name = '/'.join((nodename, 'describe_parameters')) |
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.
Same as for the parameter separator, it would be better to not redefine this hardcoded separator in every service name definition. Though it looks like that's what's done currently in rclcpp so it's not a blocker for getting this merged
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.
Constantizing this is straightforward and we've got to start it somewhere. What's a good name for the constant? TOPIC_SEPARATOR_STRING
?
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 sounds good to me. It's consistent with the other constant defined in this PR and the naming used in otherp laces like logging https://github.com/ros2/rcutils/blob/4cbd05d9c09886754f41747afa705f15d02c6818/include/rcutils/logging.h#L34
@@ -175,6 +177,70 @@ def test_set_executor_clear_executor(self): | |||
finally: | |||
node.destroy_node() | |||
|
|||
def test_node_set_parameters(self): |
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.
Based on feedback from #216 (comment) it looks like we now prefer using directly the assert statement rather than the ones provided by unittest (same multiple times below). Though not a blocker for this PR IMO
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.
last version looks good to me. Just added one question to the team below about method naming everything else is non-blocking remarks.
Note that I didn't test extensively, just reviewed the new changes
I sent an earlier version of this to @mikaelarguedas directly but here is the node I've been testing parameter services on and an equivalent params.yaml file for matching behavior to the rclcpp implementation. https://gist.github.com/nuclearsandwich/8753121711671bfa8d9cb5f718ce09bc There are some differences in the order of results when listing parameters due to the different traversal techniques between implementations but that service has no order-dependent cases (as opposed to setting parameters non-atomically where the results are ordered based on the inputs). |
@@ -16,6 +16,8 @@ | |||
from rclpy.exceptions import InvalidTopicNameException | |||
from rclpy.impl.implementation_singleton import rclpy_implementation as _rclpy | |||
|
|||
TOPIC_SEPARATOR_STRING = '/' |
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 tried to find a "sensible" place for the topic separator constant because it seems like it could be used for more than just parameter services and this was the best I could come up with based on module names. But I can either put it in parameter_services for now or if there's a place more "sensible" than this I can move it to I'm happy to field suggestions.
return self._name | ||
|
||
@property | ||
def type_(self): |
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.
@mikaelarguedas @sloretz the property is now accessed as type_
. I had initially let this slide since the generated interface code uses type
in python currently and I know that surfaced in discussion recently but can't find it to figure out where we came down.
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 recollection is that we cannot really make changes to ROS1 messages that use builtins.
But that anything not being legacy ROS1 code (and that will result in disruptive changes) should avoid using builtins names.
Part of the discussion is at ros2/build_farmer#104, bu I don't think it discuss anything outside of generated messages
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.
Makes sense. And the change is in so all's well.
7a163bd
to
3eea643
Compare
I'm able to reproduce the hang that is happening on ARM and am now investigating. |
This adds parameter services to rclpy nodes. Like rclcpp nodes, parameter services are started by default and can be turned off with the create_node/Node constructor kwarg `start_parameter_services=False`. Parameters are stored on the Node class via a `_parameters` dict attribute. The methods following methods have been added to the Node API. * `get_parameter(name)` * `get_parameters(names)` * `set_parameters(parameters)` * `set_parameters_atomically(parameters)` A Parameter class provides a python interface to individual parameters that is (hopefully) a bit more ergonomic than using the Parameter-related message types from rcl_interfaces. Changes forthcoming in later pull requests: - Add the parameters_changed_callback API - Take initial parameters from params file argument.
When checking for valid arguments in the parameters API raise TypeErrors rather than asserting on failure.
This commit adds a Parameter.Type.check method which is used to enforce the declared parameter type at runtime.
55dbbc1
to
4631f7a
Compare
Now that we know that the timeout issues on arm were related to a hang and not because we were getting close to the timeout limit. Is bumping the existing timeout still necessary ? |
I could be mistaken about failures like: https://ci.ros2.org/job/ci_linux/5031/testReport/junit/(root)/projectroot/rclpytests/ Which I thought you had mentioned were related to earlier repeats of a test run timing out. It seems like we should comfortably get by with no timeout. What's a good way to test that well? Run just the rclpy tests with a retest until fail? |
Avoids collusion/confusion with type builtin.
The test teardown method is calling rclpy.shutdown() before the nodes created here are destroyed. With the inclusion of parameter services this is causing the tests to hang after printing the following exceptions: ========================================================================================================================== 7 passed in 0.11 seconds Exception ignored in: <bound method Node.__del__ of <rclpy.node.Node object at 0x7fc321e0fd30>> Traceback (most recent call last): File "/tmp/scree/src/rclpy-mount/rclpy/rclpy/node.py", line 410, in __del__ self.destroy_node() File "/tmp/scree/src/rclpy-mount/rclpy/rclpy/node.py", line 353, in destroy_node _rclpy.rclpy_destroy_node_entity(srv.service_handle, self.handle) RuntimeError: Failed to fini 'rcl_service_t': rcl node is invalid, rcl instance id does not match, at /tmp/scree/src/ros2/rcl/rcl/src/rcl/node.c:461 Exception ignored in: <bound method Node.__del__ of <rclpy.node.Node object at 0x7fc320597828>> Traceback (most recent call last): File "/tmp/scree/src/rclpy-mount/rclpy/rclpy/node.py", line 410, in __del__ self.destroy_node() File "/tmp/scree/src/rclpy-mount/rclpy/rclpy/node.py", line 353, in destroy_node _rclpy.rclpy_destroy_node_entity(srv.service_handle, self.handle) RuntimeError: Failed to fini 'rcl_service_t': rcl node is invalid, rcl instance id does not match, at /tmp/scree/src/ros2/rcl/rcl/src/rcl/node.c:461 Exception ignored in: <bound method Node.__del__ of <rclpy.node.Node object at 0x7fc320597b70>> Traceback (most recent call last): File "/tmp/scree/src/rclpy-mount/rclpy/rclpy/node.py", line 410, in __del__ self.destroy_node() File "/tmp/scree/src/rclpy-mount/rclpy/rclpy/node.py", line 353, in destroy_node _rclpy.rclpy_destroy_node_entity(srv.service_handle, self.handle) RuntimeError: Failed to fini 'rcl_service_t': rcl node is invalid, rcl instance id does not match, at /tmp/scree/src/ros2/rcl/rcl/src/rcl/node.c:461 Destroying the nodes before the teardown runs resolves both the indefinite hang and the exceptions.
Yeah that was the initial thought as the original console output was indicating a Timeout.
The actual tests take 30 seconds to run and then hang. One way to check how close we get to the timeout is to run the tests with Connext only and |
4631f7a
to
e6c7125
Compare
Job with only connext using ros2/ci#214: |
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.
still looks good 👍
This adds parameter services to rclpy nodes. Like rclcpp nodes, parameter services are started by default and can be turned off with the create_node/Node constructor kwarg
start_parameter_services=False
.Parameters are stored on the Node class via a
_parameters
dict attribute. The methods following methods have been added to the Node API.get_parameter(name)
get_parameters(names)
set_parameters(parameters)
set_parameters_atomically(parameters)
A Parameter class provides a python interface to individual parameters that is (hopefully) a bit more ergonomic than using the Parameter-related message types from rcl_interfaces.
connects to #202