-
Notifications
You must be signed in to change notification settings - Fork 417
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
Executor does not maintain a reference to nodes #726
Comments
this may be a duplicate of 272 |
I would expect this program to run and not crash, as ticketed in #272. But i would not expect the node to persist. Internally an executor keeps a weak reference to the nodes that have been added to it. And it needs to make sure to check the weak pointer everywhere before dereferncing it. But if you don't keep your node around it should not persist just because it's been attached to an executor. In your test program you would never even be able to remove it from the executor since you don't have a reference to it to remove so it would end up persisting until the executor is destructed which is not a relevant lifetime. |
If it’s supposed to be a weak reference, the API should be taking a pointer or an In the example program, I would expect the node to be destroyed when all references are gone. Yes, this means when the user presses ctrl-c and the executor’s destructor is called. What do you mean this is not a relevant lifecycle? I guess I’m confused by this statement “But if you don't keep your node around it should not persist just because it's been attached to an executor”. Why? The only reason the node is created is to pass to an executor. When is it desirable for an executor to have a reference to a deleted node? |
@rotu is your source checkout up to date? I feel like there is a missing argument to the I implemented something similar spinning my node here. Looks like the I'm not sure that should be done. I would expect this to be the other way around, since |
My source checkout is up to date. |
I get the sentiment behind this, but I don't 100% agree. Leaning on the Core Guidelines when uncertain, I'd say taking a We could take a
Assuming that a function that takes a Ultimately the At any rate this is not how executors work and I don't think it makes sense to change that behavior, otherwise all the code which currently add's a node to an executor will need to be updated to remove it at some point otherwise the nodes would stay around as long as the executor does. That's a big breaking change with little upside in my opinion. So based on that I think the described issue with executor not holding a shared pointer rather than a weak pointer is a "won't fix" for me. However, the proposed change in API signature (to use Instead, I think it should run (as @tfoote said) but the node should still be destroyed, or maybe If you want the shortest code possible, do this: #include "rclcpp/rclcpp.hpp"
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<rclcpp::Node>("Test"));
return 0;
} Or with a custom executor: #include "rclcpp/rclcpp.hpp"
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("Test");
rclcpp::executors::SingleThreadedExecutor executor;
executor.add_node(node);
executor.spin();
return 0;
} I think we should close this as a duplicate of #272, and if desired you guys can open an issue asking for the |
@wjwwood I think that's definitely reasonable. My biggest issue, to be clear, is that I wouldn't expect something that has a Maybe something like this? I'm pretty sure this won't compile, but, in the off chance I got all the templates right to make the compiler happy, I'm still not so I don't think this is the right idea. The issue is I couldn't work around the templated return type in case the subclassed node has a different number of arguments than the base class in its constructor, as well as the fact that it is pretty important to verify that the subclassed node type is also a node type. Maybe my horrifying mess of whatever is below will inspire someone who is better at this than me to come up with something clever (or at least has a better chance of compiling than this horrible mess)? template<typename... Args>
std::shared_ptr<T> emplace_node(Args && ... args)
{
std::shared_ptr<T> node = std::make_shared<T>(std::forward<decltype(args)>(args)...);
// expensive stuff:
std::shared_ptr<rclcpp::Node> as_rclcpp_node = std::dynamic_pointer_cast<rclcpp::Node>(node);
if (nullptr == as_rclcpp_node) {
return nullptr; // for shame
}
return weak_nodes_.emplace_back(std::move(as_rclcpp_node)).lock(); // absolutely disgusting
} |
@wjwwood, the core guidelines you linked to explains exactly why add_node should not take a smart pointer:
I see your point that it would be a possibly breaking change. I do think that it makes sense to keep the node object alive while it might be still executed. If the caller knows the node is no longer needed, they have a good way to indicate that: |
I can understand where the confusion comes from, but as I said, I don't think that the actual behavior (i.e. "I [the executor] will consider this node when spinning") is a less valid interpretation given just the name and signature. And also that changing that behavior is a major behavioral change (code still compiles but does not work right) and for a fairly small benefit. That's why I don't really want to change that.
Emplace means something different in the C++ world, it means "construct and insert" in the context of a container (avoiding the need to create it locally and then copy on insertion). First of all, the executor isn't a container, instead it operates on references to the nodes, and it uses shared ownership at times to avoid some of the nastier bugs a user can run into if they destroy things out of order (or it's supposed to, it's failing to do so in this case). Second, the node is already constructed and always constructed as a We could still have the function you've proposed, but it really doesn't buy us anything I think. It's just so trivial to hold a reference to the node in the same scope that you create the executor.
That's fair and we could provide a version of As I said, we have plans to refactor the Executor so it can operate on callback groups instead of just nodes (a more granular relationship between nodes and executors), and we can reconsider these API's then. In the meantime, I'd be ok with better errors, warnings (maybe if a node is immediately created and destroyed, though that might be annoying for cases like tests), and/or updating the signature to take a |
I strongly agree. After thinking about what the factory looked like (by writing that ghastly monstrosity up there) I very quickly saw no benefits as well as confusion, stomach aches, new places for code to get caught, unpredictable behavior, and a less performant solution than simply declaring a node, then calling I'm honestly not sure what to do about this case. This is just going to be one of those threads that gets pointed at until people remember not to do it, I think. @wjwwood thank you so much for all the input. |
* Add tests proving the required functionality * Add check to the functions * Change name to test Signed-off-by: Jorge Perez <[email protected]>
Bug report
Required Info:
Steps to reproduce issue
Create a shared pointer to a Node, pass it to
SingleThreadedExecutor::add_node
. Delete or reset the shared pointer. Spin the executor.Expected behavior
The node should run.
Actual behavior
ROS crashes with an error like:
Additional information
The text was updated successfully, but these errors were encountered: