Open roger-strain opened 4 years ago
Not sure if this is the right naming or format; if there are issues, please let me know
After a comment in a team meeting, this may be too launch
specific, and it would be better to move it to the doc
folder there https://github.com/ros2/launch/tree/master/launch/doc/source.
launch
is a ROS agnostic package, so it may be good to split the documentation between launch
and [launch_ros] (https://github.com/ros2/launch_ros).
We can finish the reviewing process in this PR to avoid splitting the discussion, and split the document between launch
and launch_ros
when everything is ready (including an implementation).
Adding @wjwwood and @hidmic for feedback.
@roger-strain friendly ping
@roger-strain friendly ping
Don't worry, this was actually next on my list. Things have been a little unsettled lately. Hope to have some updates pushed this week.
@ivanpauno @hidmic
I've gone through and tried to revisit this with the comments you left in mind. I've tried to split the node definitions out so they are further isolated from the actual execution Action
itself. This ended up leaving me with a couple of independent parents, one on the Executable
side and one on the Node
side; I've tried to join them with an ExecutableNode
class, which itself inherits from Executable
, but has a Node
as a property. It's the best I could come up with having stared at this for too long, so if there's a cleaner solution, I'd be all ears.
I believe the various bits of functionality can work in this proposed architecture, but it's definitely another step or two removed from the current implementation, so I'd definitely appreciate some more experienced eyes to make sure I haven't overlooked something showstopping.
One thing I noticed as I was doing this is that as it stands, I can't put a LifecycleNode
in as part of a composable node; however, that also doesn't appear to be possible in the current system, unless I have a fundamental misunderstanding of part of the launch system. I don't discount that possibility, so please do enlighten me if I've missed something big.
I think we're getting closer @roger-strain. First of all, thank you for pushing.
I spent some time thinking on what you wrote, in comments and in the document itself. We have different concepts here that need to be represented and that today are somewhat mixed up (often just for the sake of simplicity and convenience):
launch.actions.ExecuteLocalProcess
and launch.actions.ExecuteSshProcess
actions. It could be further generalized into a single launch.actions.Execute
action that takes execution proxies or protocols, but it's fine as it stands. launch.description.Executable
and launch_ros.description.ExecutableNode
and launch_ros.description.ComposableNodeContainer
. I think we can simplify this down to launch.descriptions.Executable
and launch_ros.descriptions.Executable
. Nodes are not executables, executables bear nodes.launch_ros.description.Executable
, to be found in a ROS package, would take launch_ros.descriptions.Node
instances (a ROS aware executable always has at least one built-in node in it, or it wouldn't be ROS aware). A standard container for composition is nothing but a well known launch_ros.descriptions.Node
. Composable nodes are also just launch_ros.descriptions.Node
instances with a plugin name. So a couple launch_ros.descriptions.Node
subclasses would suffice for composition support.And then there's lifecycle nodes. You're right those are tricky and that lifecycle composable nodes are not currently supported. Personally, I think the difficulties stem from the fact we are thinking in terms of lifecycle nodes instead of nodes that have a lifecycle. IMO, having a lifecycle should be a node trait (at least in launch_ros
). Then, a launch_ros.descriptions.Node
or any of its subclasses could take a launch_ros.traits.HasLifecycle
to complete the picture.
Of course, we can come up with a bunch of handy combinations to both avoid breaking the current API and keeping the typical use cases simple. WDYT?
At first blush, I think I see where you're going, and like the general concepts. I personally would shy away from having both a launch.descriptions.Executable
and a launch_ros.descriptions.Executable
, because I would be a little concerned about confusion because of the extreme similarity between the names, but that might just be me being a little over-conservative about that sort of thing.
One question I have on this is, do we expect there to be any launch_ros.description.Executable
s other than Node
s? I think one reason I had steered toward the ExecutableNode
approach was because I didn't see it ever being anything else.
I definitely like the idea of moving the concept of ComposableNodeContainer
to being a subclass of launch_ros.description.Node
instead. That could easily take the list of launch_ros.description.ComposableNode
s as part of its argument list.
I'll have to ponder this a little more fully before I can start putting together an update. I like to make sure I've really got all the pieces sorted in my head first. But I like where this is going, and really appreciate your help in moving it forward.
One question I have on this is, do we expect there to be any
launch_ros.description.Executables
other thanNode
s? I think one reason I had steered toward theExecutableNode
approach was because I didn't see it ever being anything else.
The thing is, an executable is not a node. An executable has nodes. This is a fundamental shift from ROS 1, where there was a 1-to-1 mapping between nodes and processes. Simple executables often have only one node, but they could contain dozens. That's why I think that both concepts have to be decoupled. For sure, that doesn't mean that we can't have a launch_ros.actions.Node
-like wrapper to keep the common one-node-per-process use case as straightforward as possible.
I would be a little concerned about confusion because of the extreme similarity between the names, but that might just be me being a little over-conservative about that sort of thing.
I do see the potential source for confusion. I'm not great at naming things :sweat_smile:
I'll have to ponder this a little more fully before I can start putting together an update. I like to make sure I've really got all the pieces sorted in my head first.
Thank you for pushing !
A standard container for composition is nothing but a well known
launch_ros.descriptions.Node
.
...and actually, this isn't entirely true. Any node could implement the dynamic composition API. It's a stretch, but this could be yet another trait of a node.
Apologies, I've been redirected onto a different effort for the past several weeks, but I'm trying to spin back up on this. Now that I'm getting into the details of trying to break this down as we've discussed, there are a couple of sticky points I would love some insight on so I can properly address them now, rather than getting perplexed when trying to implement.
Simple executables often have only one node, but they could contain dozens.
This is a straightforward concept, but there's at least one part of the current implementation that bugs me. In the current launch_ros.actions.Node
, a variety of parameters can be passed as command line arguments during execution: node_name
(as __node:=
), node_namespace
(as __ns:=
), parameters
(as __params:=
), and remapping
(as {}:={}
). In the current iteration of the refactor, those fields are all captured as part of the proposed launch_ros.descriptions.Node
object (which is not itself a launch.descriptions.Executable
). Additionally, there is an arguments
parameter which is additionally passed when executing the node (in the current design).
Once we start delving into the concept of having a single executable which is host to multiple nodes (not using the composable interface you mentioned) I wonder how those should now apply. Conceivably you could specify alternate names, namespaces, parameters, remaps, etc. for the various nodes which are part of an executable. I haven't dug down into the other side of this to know if there are ways around it, so I'm hoping if there's some technique for properly passing those through and getting them to the right internal place, you can point me in the right direction. If something like specifying alternate node names for multiple nodes in a process isn't supported, then how should we approach things? I could possibly see doing something like either keeping an ExecutableNode
concept somewhere, with it (and it alone) being able to pass some of those command line arguments; whereas by using the base class with multiple node support, you explicitly give up the ability to specify those parameters. But then, the definitions are contained at the ros_launch.descriptions.Node
level, which means it would be possible to define them, even if they aren't actually usable. I've kind of twisted myself into knots on this one, so I really hope you can help untangle.
For the Composable node design, you mention that theoretically any node might implement the composition API. Would it be better to have a subclass of ros_launch.descriptions.Node
along the lines of ros_launch.descriptions.ComposingNode
, which simply adds the nodes
parameter (a list of ros_launch.descriptions.ComposableNode
s) and sends the appropriate call after launching the main node, or should that functionality just be built into ros_launch.descriptions.Node
itself, and if you choose to include a list of nodes, then you are implicitly asserting that the node supports the composition API?
For the Lifecycle side of things I've got a hopefully simple question; Python isn't my native dialect, so your mention of "traits" is intriguing to me. I've found a couple of things which I believe might be what you're referring to, but can you point me at a concrete example so I know for sure what concepts I should be exploiting here?
Once we start delving into the concept of having a single executable which is host to multiple nodes (not using the composable interface you mentioned) I wonder how those should now apply. Conceivably you could specify alternate names, namespaces, parameters, remaps, etc. for the various nodes which are part of an executable.
That's a good point, and it is supported. Take a look at the Capabilities
section of the ROS2 command line arguments design document. In short, you can target a specific node by providing its name as a prefix when specifying remaps and parameters. This is already given when using parameters file. When changing its name, the name it was given in code has to be provided. Thus, in the worst case scenario, the user has to provide the original node name along with launch_ros.descriptions.Node
description in launch
.
Would it be better to have a subclass of
ros_launch.descriptions.Node
[...] or should that functionality just be built intoros_launch.descriptions.Node
itself [...]?
IMHO inheritance is fine for composable node descriptions, but not for the container. And I'd rather not inject that functionality into the basic node description.
For the Lifecycle side of things [...] your mention of "traits" is intriguing to me.
It's not a specific Python feature, but a concept. We know that a lifecycle node has a specific behavior and interface. Same for a node that can compose other nodes. So instead of encoding all that in a Python type object, we do so in a separate "trait" object that descriptions can take.
For instance, instead of:
launch_ros.descriptions.ComposableNodesContainerWithLifecycle(
node_name='my_node',
nodes=[
launch_ros.descriptions.ComposableNode(
plugin_name='some::Node',
),
# ...
],
# ...
)
which is to say that it IS a lifecycle, composable nodes' container node, we go for:
launch_ros.descriptions.Node(
node_name='my_node',
traits=[
launch_ros.traits.HasLifecycle(),
launch_ros.traits.ComposesNodes(
nodes=[
launch_ros.descriptions.ComposableNode(
plugin_name='some::Node',
traits=[
# and so on...
]
),
# ...
]
)
],
)
which is to say that it IS a node THAT has a lifecycle and composes other nodes.
In a way, it's like a type system based on runtime data.
This is, however, my personal take on how we could tackle this problem in a scalable way. I'm sure @wjwwood and @ivanpauno have ideas too.
@hidmic Took a while for me to think this through and get somewhere I'm happy with, but I think this might be getting closer to what you're envisioning. I think there are several places where we'll want to maintain wrappers that simplify some of the structure for typical use, but I think this manages to get things split into reasonable places. There are quite a few changes in the ros_launch
sector, so probably best to come at it as a fresh whole. See what you think; I'm looking forward to getting this into shape.
@hidmic New version pushed up to address feedback. Still one point I'm not sure about w.r.t. whether package
should be required for ComposableNode
s. I also was thinking about changing ComposableNode
from being a subclass of Node
to instead being a trait, but I kind of feel like the overhead of syntax isn't really worth it there. Would like your opinion as well.
I also was thinking about changing ComposableNode from being a subclass of Node to instead being a trait, but I kind of feel like the overhead of syntax isn't really worth it there.
I agree. I think the ability to be composed is significant enough (in terms of usability and implementation) to warrant a new derived type.
@hidmic I've updated the PR based on the latest feedback. I'd been holding off to see if @ivanpauno or @wjwwood had anything else to add so I didn't clutter up the commit history too much.
How close do you think we are to getting to a point of being able to start implementation? I've held off because I'd hate to start doing work that doesn't line up with the overall project goals, but we do have some other things (most notably adding support for multi-machine launch files) that this will impact, so I'd like to give our other guys some certainty that this is the path we're going to follow. I feel like it's pretty close myself, but will be happier with a few more head-nods.
@hidmic I've updated the PR based on the latest feedback. I'd been holding off to see if @ivanpauno or @wjwwood had anything else to add so I didn't clutter up the commit history too much.
Thanks for your patience and work.
How close do you think we are to getting to a point of being able to start implementation? I've held off because I'd hate to start doing work that doesn't line up with the overall project goals, but we do have some other things (most notably adding support for multi-machine launch files) that this will impact, so I'd like to give our other guys some certainty that this is the path we're going to follow. I feel like it's pretty close myself, but will be happier with a few more head-nods.
I'll proof read this at some point this week, but I think it's pretty good as-is. Let's see what others say.
This pull request has been mentioned on ROS Discourse. There might be relevant details there:
https://discourse.ros.org/t/ros-2-tsc-meeting-minutes-2020-07-16/15468/1
@hidmic I think we've probably gotten about as far as we will with this, so I'd like to see what the next steps should be. This is a larger issue than I've tackled in a project like this, so I'm not sure what the normal approach is. I'm thinking that the changes within launch
are probably a little smaller, and should have appropriate backward-compatible interfaces, so maybe focus on that part first as an individual PR, then work on the launch_ros
portions afterward? Are PRs like this typically living items so there can be frequent reviews, or are they held until the functionality is essentially complete so it can be reviewed as a whole?
Also, if @wjwwood has a chance for any final feedback, it would be greatly appreciated.
I'm thinking that the changes within
launch
are probably a little smaller, and should have appropriate backward-compatible interfaces, so maybe focus on that part first as an individual PR, then work on the launch_ros portions afterward?
Building up support for this in launch
first sounds reasonable to me. But I'd encourage you to split the work across multiple PRs. New classes and concepts can coexist with existing ones, even if and until the latter become a shell for the first. Small, incremental contributions are also easier to review, which means you'll get better feedback and faster than if you submit a single massive PR.
I'd also recommend you clearly state what the PR series is going to look like for reviewers' context.
Are PRs like this typically living items so there can be frequent reviews, or are they held until the functionality is essentially complete so it can be reviewed as a whole?
In general, you don't want to hold back any significant amount of work until it's "done" (whatever the definition of done is). Architectural changes require early review and discussion, and this design document has already built some consensus. Implementation wise, complying with contribution guidelines and getting early feedback from OSRF and/or the community will overall result in less re-work.
So, if your PR is medium sized or you come across some oversight in this document, submitting a draft is perhaps a good idea. But if you keep PRs small, you may as well just open them.
@ivanpauno I've been working up a plan for how the PRs might look for this, and would appreciate a once over. Also have a few questions towards the end about things I think need to be dealt with, but that we haven't discussed yet. (Apologies that this is long, just wanting to make sure I have things covered.)
Conflicting classes:
arguments
Planned PR sequence:
Questions:
The overall plan sounds good to me, I have some minimal comments:
All original parameters/properties still exist, though one is renamed (extra_arguments -> arguments)
You could still accept the old argument name, with a deprecation warning.
Add backward compatibility
Maybe, we can create a "feature" branch in launch
and launch_ros
, and don't merge it on master until a coherent subset of things is merged.
For example:
In that way, we ensure we're only merging to master
classes that can already be used.
The risk of having conflicts is low, as each time a "refactor" step is done the feature branch is merged into master.
Add tests to launch_ros/launch_testing_ros as possible.
Instead of adding testing in a separate step, each PR should test the class/feature is being added.
Extensiveness of unit testing? Haven't dived in yet, not sure if tests should be launching processes, nodes, etc. to verify behavior.
Tests should launch process/nodes/etc to verify.
The one where is most important to test well that launching a something is working is ExecuteLocal
.
With things like RosExecutable
, unit testing that the result of the description is correct without launching it is fine (though ideally, it's good to actually try launching something).
Introduce backward compatibility as features become complete? Guessing not, because there might be some ill-defined behavior if only some classes expect the new structure.
I'm not sure if I understand the question. I think I've just suggested to do the opposite.
Support for XML/YAML launch files? Haven't discussed this at all yet, so not sure how to approach that, and whether it should be baked in during the PRs or added late.
Don't break the existing XML/YAML support :joy:. For the moment, you won't need to extend it.
I think we're actually pretty close to the same page here.
In the list I put together, the top level items basically represented a conceptual PR. So the testing would be part of the same PR as the class(es) being implemented. I mostly wanted to make sure I was representing adding those tests during the process, not as one block at the end.
The order you put together actually exposes one of my concerns regarding backward compatibility. I don't want to, for instance, introduce a backward compatible Node until we also have LifecycleNode ready to go, for instance. Since the current LifecycleNode inherits from the current Node, I get slightly nervous thinking about whether it will still behave properly once the internals of the class are replaced. It should be backward compatible from the user/API perspective, but might (likely won't) be from the perspective of an inheriting class.
And I will gladly stay hands-off from the XML/YAML for now. :)
The order you put together actually exposes one of my concerns regarding backward compatibility. I don't want to, for instance, introduce a backward compatible Node until we also have LifecycleNode ready to go, for instance. Since the current LifecycleNode inherits from the current Node, I get slightly nervous thinking about whether it will still behave properly once the internals of the class are replaced. It should be backward compatible from the user/API perspective, but might (likely won't) be from the perspective of an inheriting class.
It shouldn't be a problem.
In that case for example, I think LifecycleNode
is only using the parent class constructor, execute, the node_name
property, one private method. If it's using something else, it shouldn't be too hard to fix it.
My suggestion is because I prefer introducing the "brand new" way, without introducing duplicated code. That make things easier to maintain.
Your code will also be exercised with all existing tests for Node
, which is also a good idea.
Either way, the suggested order can be modified through the process if we find a problem. The overall plan LGTM!
Maybe, we can create a "feature" branch in
launch
andlaunch_ros
, and don't merge it on master until a coherent subset of things is merged.
Indeed. This is probably the best path forward. We can create the branch once you submit the first PR of each subset.
What is the current progress on this proposed refactoring. I would like to use lifecycle nodes in a component but as far as I can see that is still not possible right now (or did I miss something?) Thanks in advance for your answer!
What is the current progress on this proposed refactoring. I would like to use lifecycle nodes in a component but as far as I can see that is still not possible right now (or did I miss something?) Thanks in advance for your answer!
A lot of the work is done at this point, although not the lifecycle portion just yet. We're trying to sort out a project issue behind the scenes to get the relevant PRs freed up and finalized. So still in the works, but not there just yet.
What is the current progress on this proposed refactoring. I would like to use lifecycle nodes in a component but as far as I can see that is still not possible right now (or did I miss something?) Thanks in advance for your answer!
A lot of the work is done at this point, although not the lifecycle portion just yet. We're trying to sort out a project issue behind the scenes to get the relevant PRs freed up and finalized. So still in the works, but not there just yet.
Awesome, thanks for your quick response and work so far. Let me know if you need help with reviewing or testing the PR(s)!
Not sure if this is the right naming or format; if there are issues, please let me know. Already had a couple of bits of feedback from @ivanpauno that are not yet addressed, but wanted to hopefully give this higher visibility so we might be able to start implementation of the concept if things look reasonable.
Design to address ros2/launch#114