Implement node parameters and parameter services#214
Conversation
mikaelarguedas
left a comment
There was a problem hiding this comment.
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.
should we raise a TypeError exception rather than asserting ?
There was a problem hiding this comment.
I wasn't certain if TypeErrors 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.
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.
| 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.
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.
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.
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.
This line of review is resolved by adding creation-time type checking in 39698fb.
| 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.
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.
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.
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.
| nodename = node.get_name() | ||
|
|
||
| describe_parameters_service_name = '/'.join((nodename, 'describe_parameters')) | ||
| node.create_service( |
There was a problem hiding this comment.
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
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 |
mikaelarguedas
left a comment
There was a problem hiding this comment.
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.
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.
'.' 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.
It looks like this comment is still pending (same for other uses of '.' below)
There was a problem hiding this comment.
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.
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.
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.
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.
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. |
| 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.
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.
maybe @wjwwood has more context regarding the use of the prefixes field in the result message?
There was a problem hiding this comment.
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
prefixesfield 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.
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.
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.
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.
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.
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.
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
| finally: | ||
| node.destroy_node() | ||
|
|
||
| def test_node_set_parameters(self): |
There was a problem hiding this comment.
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
mikaelarguedas
left a comment
There was a problem hiding this comment.
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). |
| 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.
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.
@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.
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.
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: |
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
_parametersdict 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