(refactor) add rcl_lifecycle package - #91
Conversation
2b316e2 to
7a71150
Compare
91ef95e to
16ea839
Compare
734a92f to
de669fe
Compare
wjwwood
left a comment
There was a problem hiding this comment.
Other than some nitpicks and questions, the code looks good to me. The only thing I think must change is the error handling and how the progress is rolled back in the init function comments I had.
| find_package(lifecycle_msgs REQUIRED) | ||
|
|
||
| include_directories( | ||
| include) |
There was a problem hiding this comment.
nitpick: this doesn't need to be wrapped
| # :type var_prefix: string | ||
| # | ||
| # Copy/pasted from rclcpp/cmake/get_rclcpp_information.cmake. | ||
| # Code duplication is evil. Don't do this at home, kids. |
There was a problem hiding this comment.
This probably deserves a TODO so that we don't forget to deduplicate this code at some point.
| * @param state: integer giving the state | ||
| * @param label: label for easy indexing | ||
| */ | ||
| typedef struct _rcl_lifecycle_state_t |
There was a problem hiding this comment.
nitpick: I don't think it is necessary to use a leading _ here, or at least the rest of rcl doesn't do it this way.
| #endif | ||
|
|
||
| /** | ||
| * @brief simple definition of a state |
There was a problem hiding this comment.
Elsewhere we use this doxygen pattern:
/// Brief description on a single line.
/* Detailed description, with multiple
* lines if necessary.
*
* \param foo we use the \ rather than the @ style escapes too.
*/
We can discuss which to use, but I believe I decided to use the above as it is the recommend "C++ style" rather than the "Java style" which you're using here. Either way, it would be good to have one style throughout the packages.
| /** | ||
| * @brief transition definition | ||
| * @param start: rcl_lifecycle_state_t as a start state | ||
| * @param goal: rcl_lifecycle_state_t as a goal state |
There was a problem hiding this comment.
I'm not sure you should use the @param directive to describe members of the struct. There is a member documentation syntax for doxygen, see http://www.stack.nl/~dimitri/doxygen/manual/docblocks.html (search for Putting documentation after members as there doesn't seem to be an achor).
| if (ret != RCL_RET_OK) { | ||
| fcn_ret = RCL_RET_ERROR; | ||
| } | ||
| lifecycle_msgs__msg__TransitionEvent__fini(&msg); |
There was a problem hiding this comment.
If you were going for reverse order destruction, then this message should be finalized before the publisher.
| const rcl_lifecycle_state_t * start, const rcl_lifecycle_state_t * goal) | ||
| { | ||
| msg.start_state.id = start->id; | ||
| rosidl_generator_c__String__assign(&msg.start_state.label, start->label); |
There was a problem hiding this comment.
This function returns a bool which should be checked.
| // limitations under the License. | ||
|
|
||
| #ifndef COM_INTERFACE_HXX_ | ||
| #define COM_INTERFACE_HXX_ |
There was a problem hiding this comment.
that addresses a linter complaint when including private headers. The linter requires an include folder with it. The linter only checks .h and .hpp though. Is there a recommended way of doing so?
There was a problem hiding this comment.
Can you give an example of this? I'm not familiar with the linter error you're talking about.
There was a problem hiding this comment.
4:[...] Include the directory when naming .h files [build/include] [4]
There was a problem hiding this comment.
test_communication does lint its files (https://github.com/ros2/system_tests/blob/8074a4128b785c6c2376ec432e94d8bc5e85ff04/test_communication/CMakeLists.txt#L16) and uses a local header too (https://github.com/ros2/system_tests/blob/8074a4128b785c6c2376ec432e94d8bc5e85ff04/test_communication/test/message_fixtures.hpp). And somehow that passes 😉
|
|
||
| // add transitions to map | ||
| // TODO(karsten1987): Add global transition for unknown/resetting | ||
| // TODO(karsten1987): Pointer comparison fails here because of copy! |
There was a problem hiding this comment.
Does this need to be addressed before merging?
| return RCL_RET_ERROR; | ||
| } | ||
|
|
||
| // TODO(Karsten1987): pointer comparison fails here |
There was a problem hiding this comment.
Again, should this be addressed before a merge?
|
Thanks for the review. I'll address the comments asap and reopen this PR for review. |
dirk-thomas
left a comment
There was a problem hiding this comment.
Throughout the code we use the extension .hpp, not .hxx.
| // and limited in length of 255 | ||
| const char * topic_prefix = "__transition_notify"; | ||
| char * topic_name; | ||
| if (concatenate(&node_name, &topic_prefix, &topic_name) != true) { |
There was a problem hiding this comment.
The comparison with != true can be written simpler by just prefixing the call with !.
There are similar variations throughout the patch like == true, == NULL, etc.
|
|
||
| { // initialize publisher | ||
| // Build topic, topic suffix hardcoded for now | ||
| // and limited in length of 255 |
There was a problem hiding this comment.
The three space indentation looks weird. Maybe don't put the first comment in line 85 but into a new line. Than it can have consistent indentation with the block.
Same below.
|
|
||
| RCL_LIFECYCLE_PUBLIC | ||
| rcl_ret_t | ||
| rcl_lifecycle_state_machine_init(rcl_lifecycle_state_machine_t * state_machine, |
| // start label and classify them. | ||
| const rcl_lifecycle_state_t * start_state = rcl_lifecycle_get_primary_state(m, start_id); | ||
| if (start_state == NULL) { | ||
| // return false here? |
There was a problem hiding this comment.
Should this be addressed and the return type of the function be changed?
|
I updated most of the comments mentioned. @gbiggs Thanks for your comment. I changed the state machine transitions in a way that is hopefully easier to grabs. All transitions are now equally implemented. This leads that a transition is composed of two individual steps by now: example: 1.) transition from
2.) transition from
3.) in case of error, two more transitions are available
|
|
@Karsten1987 Thanks for working to make the state machine implementation clearer. I still think that it is too complex, though. I think that implementing it as a plain state machine is sufficient. Equipping every transition with two destinations is not how state machines are typically designed or implemented. Rather, every state should just be a state, and every transition should just be from one state to another state, with no deviations. The current implementation will be confusing for anyone who knows how state machines work because their assumptions will be incorrect. This and the additional complexity mean it is less maintainable and less reusable. It will also struggle if at any point we have states with more than two possible transitions out. The movement from the
This can be achieved by making the state machine machinery execute a registered function for each state. Each function would be responsible for controlling which next transition is executed and when. Some of those functions would also call user-provided callbacks for the node behaviour. |
a9ee85c to
1cc68ff
Compare
|
@gbiggs I am sorry for not being clear at this point. The current implementation is pretty much exactly the way you described it.
Then again, this is shared C code, which serves as a base for high level languages. The current rclcpp api https://github.com/ros2/rclcpp/blob/lifecycle_impl/rclcpp_lifecycle/test/test_lifecycle_node.cpp provides then an user friendly interface, which internally checks whether a callback returned successfully or not. |
|
@Karsten1987 I did look at the implementation, and it is not as I attempted to describe it. The effect is the same, but the implementation does not have certain states checking for callback results and choosing the next state. It has the transition checking the result and choosing the next state. It is implemented as transitions with "on transition" actions (which are often used in state machines) but two possible destinations based on the result. A standard FSM may execute something when a transition occurs but the result of that action does not have any impact on the next state. Only the actions executed in the state can have an impact on the next state. This is why I think the current implementation is not correct. Instead of transitions with callbacks, with two states The current implementation does work for the current life cycle state machine, because it just happens that all the transitions between important life cycle states go via states with callbacks that may fail. But I think it is likely that someone will want to implement a custom life cycle for their nodes at some point. It would be nice if the implementation would be generic so that any desired life cycle state machine can be implemented. If this is not a goal, then I think we should just have a hard-coded state machine, which means getting rid of things like Also, configurable error states are fine, but they should be done by altering the state machine, not by having transitions with multiple possible destinations. I re-made the state machine diagram that Tully (I think it was) made making it clearer where functions are called and when transitions happen. Remember that if a particular transition is not triggered, then the state machine does not move. |
|
As far as I can see, each transition (made by calling
We could refactor that function to not take the https://github.com/ros2/rcl/blob/lifecycle_impl/rcl_lifecycle/src/rcl_lifecycle.c#L165-L171 and have explicit transitions between each appropriate state and the error processing state. However, I do not believe that the implemented system has any other ambiguity of what the end state can be, i.e. the transitions do not determine what the next non-error state will be, there is always exactly one. Separately from that, I think that your diagram you linked is incorrect because it says that the states like configuring and activating go to the error processing state if their callback (e.g. onConfigure and onActivate) fails. From my perspective, the state "Activating" calls a callback "onActivate" which the user can override and if the user returns true, then it transitions to the active state, and if they throw an exception (or otherwise indicate an unhandled error) then it will trigger a transition to the error processing state, but (and this is where your diagram departs from the diagram in the design doc I think) if the user returns false for "onActivate" then the Activating state should trigger a transition back to the inactive state. @tfoote can confirm that this is the correct behavior, but I think it is because otherwise it would be impossible to fail an activation and not end up in the unconfigured or finalized state. It's also pretty clear to me that this is what is described in the design doc's diagram. Now, @Karsten1987's implementation doesn't do that as-is but that's another issue 😛. Currently this pr implements it such that returning false by the user in either onConfigure or onActivate will result in a transition to the error processing state and from there to either the unconfigured or finalized state. This might be ok for onConfigure because it would have ended up in the unconfigured state anyways, but I do not believe this is the desired end behavior for onActivate. As for having a generic FSM implementation, I agree that this is not that. If we wanted that then we would need to refactor the "change_state" method in the C++: Currently this function has a few assumed preconditions which hold true for our state machine, but do not hold true for all FSM:
So this is some simple, but implicit logic that is baked into our state machine which means that all valid start states transition to "autonomous" states which do not require external stimulus to then transition to another valid "start" state. This function takes these steps:
While this is not a generic FSM, I do not come to the same conclusion as you @gbiggs (that the transitions are responsible for deciding the next state or that the transitions can fail) because it is actually the callback of the state (configuring or activating, etc.) which determines the next state and not the transition itself. To make it more generic we would want to refactor it such that it:
Then in the implementation of the callback for the current states we would cause additional transitions if appropriate. For example the callback of the Activating state might:
A similar set of logic would then be implemented for each state, like Configuring, Deactivating, Unconfiguring, etc. The callback for other states like unconfigured, inactive, and active may contain some logic, but by design would not schedule a new transition themselves, but rather would wait for external stimulus to do so. With all of that being said, I do not think we need to refactor the state machine to behave like this right now. I think this because I see this as largely an implementation detail. In either case I think the user will implement callbacks for some states and not for others and they will manually trigger transitions out of some states but not for others, and that this will not change materially if we change the state machine as described above. The caveat to that is that we may need to do this refactor now in order for @Karsten1987 to change the behavior to match the document, i.e. so that a return of false from onActivate returns you to the inactive state not the error processing state, but that isn't clear to me. |
I based my diagram on the text specification at http://design.ros2.org/articles/node_lifecycle.html rather than the diagram. As an example, for the
I have no problems with changing the behaviour to allow a I do not think that the current implementation needs to be refactored as I have asked to enable this. The function controlling the transition can do it already with a minor change. |
I think I can see that my waffling comments are not comprehensible enough. 😄 I was trying to abstract from the implementation to the effective result. What you propose is pretty much what I am asking for.
Yes, I agree with this point. The implementation of this specific state machine is not user-facing, so long as the user is happy with the existing state machine. |
Ahhhh, you're right the diagram and text are in disagreement. Talking with @tfoote I think the text suffers from a copy-paste error. I'll coordinate with @Karsten1987 to address this issue, either by leaving it as-is for now and plan to address it after the beta or work with him to update the text in the document and his implementation here to get consistent behavior. Also we're gonna try to merge your pr against that design doc. |
wjwwood
left a comment
There was a problem hiding this comment.
Other than the existing comments (about the indention and the .hxx file, etc.) and the apparently reverting commits, both of which are just logistical things, this lgtm.
| } | ||
|
|
||
| rcl_ret_t | ||
| rcl_trigger_guard_condition(const rcl_guard_condition_t * guard_condition) |
There was a problem hiding this comment.
These changes seem to undo recent commits to master. I think you need to make sure your rebase worked correctly...
There was a problem hiding this comment.
Actually, they don't undo master, but they do the same as master. I think they'll just disappear on merge, but if you have an opportunity to rebase before the final CI run, you can get rid of this commit that way.
a5ec0e7 to
afa9a81
Compare
* misra rule 10.1 non-boolean comparison * misra c rule 8.2 (partial) * misra c rule 14.4 * fix shadowing issue on ret variable * match argument name in header and c file
Connects to ros2/rclcpp#265