Skip to content
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

Pass goal handle to goal response callback instead of a future #1311

Merged
merged 6 commits into from
Sep 18, 2020

Conversation

jacobperron
Copy link
Member

This resolves an issue where promise->set_value is called before a potential call to promise->set_exception.
If there is an issue sending a result request, set the exception on the future to the result in the goal handle, instead of the future to the goal handle itself.

Alternative to #1292.

Copy link
Contributor

@brawner brawner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems reasonable to me. I'll let @hidmic have the final approval though.

std::lock_guard<std::mutex> lock(goal_handles_mutex_);
goal_handles_.erase(goal_handle->get_goal_id());
});
} catch (rclcpp::exceptions::RCLError &) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jacobperron what if an exception of a different type gets thrown?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but I think this is the only type that can be thrown, here.

goal_handles_.erase(goal_handle->get_goal_id());
});
} catch (rclcpp::exceptions::RCLError &) {
goal_handle->invalidate();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jacobperron hmm, my only concern here is that the caller won't know that the result request went wrong till much later. It also means a caller can't cancel the goal just sent.


What if we just eat up the exception? It's not ideal, but ClientGoalHandle::is_result_aware() can still return false and ClientGoalHandle::async_get_result() (which will eventually be called anyways) can throw.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's not as ergonomic, but the caller can still immediately check the future returned by Client::async_get_result, and any error that occurred when sending the result request should be visible.

It also means a caller can't cancel the goal just sent.

This is a valid concern. I think what you're proposing is to remove the erasure logic below so that the user can still interact with the goal handle (even though it failed to get the result). I think calling invalidate() still makes sense so that calling Client::async_get_result still produces an exception.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other option I hadn't considered was to not handle the exception at all. This way we leave it up to the caller how to handle the exception (and they know something went wrong right away).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. If ClientGoalHandle::make_result_aware fails, the goal handle will not be result aware. Any attempt to get the future will result in an exception that the user can handle.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take a look at 81239f1

As it is currently implemented, the user will get the exception set in the future (because we set result awareness to true above, before sending a result request).

But, I think it makes sense to throw the error as earlier if possible, so I've make a small change make this happen: d444e09

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's anything else to do 🤔 If a user attempts to request the result I'm not actually sure what would happen, but I guess it shouldn't be allowed considering the result future would have already been set to an exception. In retrospect, I think it keeps the logic simpler to let the exception live in the future (1176dad).
Currently, the user is not able to request the result more than once (even if it is successful).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see. We're not dealing with exceptions correctly e.g. depending on where Client::async_get_result() throws, the handle may be tagged as result aware (banning future result requests) or not.

In my mind, a user that explicitly makes a goal handle result aware should get an exception if something goes wrong, and the goal handle should remain result unaware. That is, Client::async_get_result() should offer strong exception guarantees. It shouldn't be different for a user that implicitly makes a goal handle result aware by passing in a result callback to Client::async_send_goal(). If we don't make the latter throw (and there're valid arguments against), by keeping the goal handle result unaware we allow the user to check ClientGoalHandle::is_result_aware() or handle the exception that ClientGoalHandle::async_get_result() will throw. Does that make sense?

To invalidate the goal handle improves the situation quite a bit, but it delays error generation (until future::get, when it could show itself before spinning), it induces an spurious status change (to UNKNOWN until another status message comes along), and it prevents any retries on an otherwise unaware goal handle.

But I won't block you. CI's green and this is a fairly unusual case :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense to me. I've made changes so that in the event that a result request fails, ClientGoalHandle::is_result_aware() should return false and Client::async_get_result() raises an exception (9cb1776).

I couldn't find a nice way to do it with the existing state information, so I've introduced a new member to ClientGoalHandle for checking if invalidate() was called, which doubles as an improved error message.

PTAL

Copy link
Contributor

@hidmic hidmic Sep 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat! Two questions. Could a goal handle be invalidated twice? Should we do something about the spurious UNKNOWN status on invalidation? Anyhow, LGTM.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions. I think we certainly want to guard against multiple calls invalidate (see b145c73). As an aside, I wish it was easier to test this stuff 🙃 It might be worth revisiting the architecture to make validation easier.

I'm not sure what to do about the UNKNOWN status. We could remove setting it when the goal is invalidated. Really I think the status should be reporting the latest state from the action server. Since this is keeping the same behavior (ie. when the client is destroyed), I'll defer deciding to remove the unknown status to another PR.

brawner added a commit that referenced this pull request Sep 14, 2020
Signed-off-by: Stephen Brawner <brawner@gmail.com>
@brawner
Copy link
Contributor

brawner commented Sep 14, 2020

Here are the builds from #1290. I'm still trying to figure out the Windows issue (if it pops up)

  • Linux Build Status
  • Linux-aarch64 Build Status
  • macOS Build Status
  • Windows Build Status

@jacobperron
Copy link
Member Author

jacobperron commented Sep 14, 2020

Note to self: examples, demos, and other packages that register a goal response callback need to be updated.
I'll spend a little time looking at solutions for maintaining, but deprecated, the old signature.

This resolves an issue where `promise->set_value` is called before a potential call to `promise->set_exception`.
If there is an issue sending a result request, set the exception on the future to the result in the goal handle, instead of the future to the goal handle itself.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
This way the user can still interact with the goal (e.g. send a cancel request).

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
This will cause an exception when trying to get the future to result, in addition to the exception when trying to access values for existing references to the future.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
…d due to a failed result request

Propagate error message from a failed result request.

Also set result awareness to false if the result request fails so the user can also check before
being hit with an exception.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
jacobperron added a commit to ros2/demos that referenced this pull request Sep 15, 2020
The signature changed in ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
jacobperron added a commit to ros2/examples that referenced this pull request Sep 15, 2020
The signature changed in ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
jacobperron added a commit to ros2/geometry2 that referenced this pull request Sep 15, 2020
The signature changed in ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
jacobperron added a commit to ros/ros_tutorials that referenced this pull request Sep 15, 2020
The signature changed in ros2/rclcpp#1311

Also remove related dead code.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
@jacobperron
Copy link
Member Author

Here's of list of downstream changes required by this change:

I couldn't find a nice way to deprecate the old signature. Suggestions welcome that would make the change easier on users.

brawner added a commit that referenced this pull request Sep 15, 2020
Signed-off-by: Stephen Brawner <brawner@gmail.com>
jacobperron added a commit to ros/ros_tutorials that referenced this pull request Sep 15, 2020
The signature changed in ros2/rclcpp#1311

Also remove related dead code.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
Signed-off-by: Jacob Perron <jacob@openrobotics.org>
@jacobperron jacobperron merged commit bf1396b into master Sep 18, 2020
@delete-merged-branch delete-merged-branch bot deleted the jacob/actions_refactor branch September 18, 2020 23:53
@jacobperron
Copy link
Member Author

Err, I may have merged too early. The connected PRs need approvals too 😨

jacobperron added a commit to ros2/geometry2 that referenced this pull request Sep 21, 2020
The signature changed in ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
jacobperron added a commit to ros2/examples that referenced this pull request Sep 21, 2020
The signature changed in ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
@ivanpauno
Copy link
Member

ivanpauno commented Sep 21, 2020

New build failure in nightlies seem to be related to this: https://ci.ros2.org/view/nightly/job/nightly_linux_release/1681/.

clalancette pushed a commit to ros2/demos that referenced this pull request Sep 21, 2020
The signature changed in ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
clalancette pushed a commit to ros/ros_tutorials that referenced this pull request Sep 21, 2020
The signature changed in ros2/rclcpp#1311

Also remove related dead code.

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
jacobperron added a commit to ros2/ros2_documentation that referenced this pull request Sep 21, 2020
Related PR: ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
@jacobperron
Copy link
Member Author

Added a release note for Galactic: ros2/ros2_documentation#871

brawner added a commit that referenced this pull request Sep 21, 2020
Signed-off-by: Stephen Brawner <brawner@gmail.com>
jacobperron added a commit to ros2/ros2_documentation that referenced this pull request Sep 21, 2020
* Add note about rclcpp_action API change in Galactic

Related PR: ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix typo

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
brawner added a commit that referenced this pull request Sep 22, 2020
* Increase coverage rclcpp_action to 95%

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* PR fixup

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Address PR Feedback

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Rebase onto #1311

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* rcutils test depend

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Cleaning up

Signed-off-by: Stephen Brawner <brawner@gmail.com>
ahcorde pushed a commit that referenced this pull request Oct 22, 2020
* Increase coverage rclcpp_action to 95%

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* PR fixup

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Address PR Feedback

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Rebase onto #1311

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* rcutils test depend

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Cleaning up

Signed-off-by: Stephen Brawner <brawner@gmail.com>
ahcorde pushed a commit that referenced this pull request Oct 26, 2020
* Increase coverage rclcpp_action to 95%

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* PR fixup

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Address PR Feedback

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Rebase onto #1311

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* rcutils test depend

Signed-off-by: Stephen Brawner <brawner@gmail.com>

* Cleaning up

Signed-off-by: Stephen Brawner <brawner@gmail.com>
@v-lopez
Copy link

v-lopez commented Nov 17, 2020

I apologise for coming back to this merged changes, but have you found any way of keeping compatibility between new and old code? Or the only solution right now is a branch for foxy and new developments on a separate branch?

@jacobperron
Copy link
Member Author

@v-lopez Unfortunately, the original bug was "easily" fixed by changing API. In this case, I think the API break makes sense since the original API of passing a future was not necessary and part of the bug (see discussion in #1292 (comment)).

I think we can easily maintain backwards compatibility if we can use C++17, specifically std::variant. E.g. change the type of the goal callback member of the SendGoalOptions struct to a std::variant. I don't know if we've settled on the minimal C++ standard for Galactic. If we can use C++17, I'm happy to add a change to make this backward compatible.

@v-lopez
Copy link

v-lopez commented Nov 18, 2020

In my use cases I've managed to workaround it using the returned shared_future from sending the goal.
My idea was more on the lines of adding the new callback to the foxy version somehow, since it is LTS a lot of packages are going to use it for the next years, and the old use was incorrect so I agree that it should be deprecated.

@jacobperron
Copy link
Member Author

I don't think adding the new callback to Foxy can be done in an ABI (or API) compatible way. Also, my understanding is the error this bug causes happens rarely (if at all), so I think it's very unlikely that we would backport this change.

@jacobperron
Copy link
Member Author

@v-lopez Please correct me if I'm wrong. If you are experiencing errors due to the bug this change fixes, perhaps we could patch it in Foxy a different way.

@v-lopez
Copy link

v-lopez commented Nov 18, 2020

No, I've not experienced the bug at all.

Was just looking for a way to make the code compatible Foxy and onwards, luckily there's multiple ways of getting the GoalHandle, we can go with that. Thanks for your efforts.

@ivanpauno
Copy link
Member

ivanpauno commented Nov 18, 2020

I think we can easily maintain backwards compatibility if we can use C++17, specifically std::variant. E.g. change the type of the goal callback member of the SendGoalOptions struct to a std::variant. I don't know if we've settled on the minimal C++ standard for Galactic. If we can use C++17, I'm happy to add a change to make this backward compatible.

Though using std::variant is nice, for this case we can manually implement an API backwards compatible adapter:

  struct SendGoalOptions
  {
    SendGoalOptions()
    : goal_response_callback(nullptr),
      feedback_callback(nullptr),
      result_callback(nullptr)
    {
    }

    /// Function called when the goal is accepted or rejected.
    /**
     * Takes a single argument that is a goal handle shared pointer.
     * If the goal is accepted, then the pointer points to a valid goal handle.
     * If the goal is rejected, then pointer has the value `nullptr`.
     */
    GoalResponseCallbackCompatibilitySelector goal_response_callback;

    /// Function called whenever feedback is received for the goal.
    FeedbackCallback feedback_callback;

    /// Function called when the result for the goal is received.
    ResultCallback result_callback;
  };

class GoalResponseCallbackCompatibilitySelector {
public:
  // implicit constructor
  [[deprecated("use new goal callback signature")]]
  GoalResponseCallbackCompatibilitySelector(GoalResponseCallbackOldSignature old_callback) : old_callback_(old_callback)
{}

  // implicit constructor
  GoalResponseCallbackCompatibilitySelector(GoalResponseCallbackNewSignature new_callback) : new_callback_(new_callback)
{}

private:
  friend rclcpp_action::Client;  // to avoid exposing getters of new_callback and old_callback publicly

  // Consumer of the class will first try to use new_callback_ and if it is `nullptr`, old_callback_ will be used.
  GoalResponseCallbackNewSignature new_callback_;
  GoalResponseCallbackOldSignature old_callback_;
};

Considering this change might affect several users, I think it's worth going through a ping-pong deprecation cycle.

@jacobperron
Copy link
Member Author

jacobperron commented Nov 18, 2020

Thanks for the suggestion, @ivanpauno 🙇
I agree, we should avoid a hard break and go through a deprecation cycle. Would you like to open a PR applying your suggestion? Otherwise, I'm happy to do it.

@ivanpauno
Copy link
Member

@v-lopez see #1495, which enabled backwards compatibility.

ferranm99 added a commit to ferranm99/ferran-ros that referenced this pull request May 20, 2022
* Add note about rclcpp_action API change in Galactic

Related PR: ros2/rclcpp#1311

Signed-off-by: Jacob Perron <jacob@openrobotics.org>

* Fix typo

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants