Skip to content

Commit 839348c

Browse files
jmachowinskiJanosch Machowinski
andauthored
Callback after cancel (#2281)
* feat(Client): Added function to stop callbacks of a goal handle This function allows us to drop the handle in a locked context. If we do not do this within a lock, there will be a race condition between the deletion of the shared_ptr of the handle and the result / feedback callbacks. * fix: make Client goal handle recursive This fixes deadlocks due to release of goal handles in callbacks etc. * fix(ActionGoalClient): Fixed memory leak for nominal case This fixes a memory leak due to a self reference in the ClientGoalHandle. Note, this fix will only work, if the ClientGoalHandle ever receives a result callback. * doc: Updated documentation of rclcpp_action::Client::async_send_goal * docs: Made the async_send_goal documentation more explicit Signed-off-by: Janosch Machowinski <[email protected]> Co-authored-by: Janosch Machowinski <[email protected]>
1 parent 90e4511 commit 839348c

File tree

2 files changed

+81
-13
lines changed

2 files changed

+81
-13
lines changed

rclcpp_action/include/rclcpp_action/client.hpp

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,22 @@ class Client : public ClientBase
405405

406406
/// Send an action goal and asynchronously get the result.
407407
/**
408-
* If the goal is accepted by an action server, the returned future is set to a `ClientGoalHandle`.
408+
* If the goal is accepted by an action server, the returned future is set to a `GoalHandle::SharedPtr`.
409409
* If the goal is rejected by an action server, then the future is set to a `nullptr`.
410410
*
411-
* The returned goal handle is used to monitor the status of the goal and get the final result.
412-
* It is valid as long as you hold a reference to the shared pointer or until the
413-
* rclcpp_action::Client is destroyed at which point the goal status will become UNKNOWN.
411+
* The goal handle in the future is used to monitor the status of the goal and get the final result.
412+
*
413+
* If callbacks were set in @param options, you will receive callbacks, as long as you hold a reference
414+
* to the shared pointer contained in the returned future, or rclcpp_action::Client is destroyed. Dropping
415+
* the shared pointer to the goal handle will not cancel the goal. In order to cancel it, you must explicitly
416+
* call async_cancel_goal.
417+
*
418+
* WARNING this method has inconsistent behaviour and a memory leak bug.
419+
* If you set the result callback in @param options, the handle will be self referencing, and you will receive
420+
* callbacks even though you do not hold a reference to the shared pointer. In this case, the self reference will
421+
* be deleted if the result callback was received. If there is no result callback, there will be a memory leak.
422+
*
423+
* To prevent the memory leak, you may call stop_callbacks() explicit. This will delete the self reference.
414424
*
415425
* \param[in] goal The goal request.
416426
* \param[in] options Options for sending the goal request. Contains references to callbacks for
@@ -448,7 +458,7 @@ class Client : public ClientBase
448458
std::shared_ptr<GoalHandle> goal_handle(
449459
new GoalHandle(goal_info, options.feedback_callback, options.result_callback));
450460
{
451-
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
461+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
452462
goal_handles_[goal_handle->get_goal_id()] = goal_handle;
453463
}
454464
promise->set_value(goal_handle);
@@ -466,7 +476,7 @@ class Client : public ClientBase
466476
// To prevent the list from growing out of control, forget about any goals
467477
// with no more user references
468478
{
469-
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
479+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
470480
auto goal_handle_it = goal_handles_.begin();
471481
while (goal_handle_it != goal_handles_.end()) {
472482
if (!goal_handle_it->second.lock()) {
@@ -496,7 +506,7 @@ class Client : public ClientBase
496506
typename GoalHandle::SharedPtr goal_handle,
497507
ResultCallback result_callback = nullptr)
498508
{
499-
std::lock_guard<std::mutex> lock(goal_handles_mutex_);
509+
std::lock_guard<std::recursive_mutex> lock(goal_handles_mutex_);
500510
if (goal_handles_.count(goal_handle->get_goal_id()) == 0) {
501511
throw exceptions::UnknownGoalHandleError();
502512
}
@@ -531,7 +541,7 @@ class Client : public ClientBase
531541
typename GoalHandle::SharedPtr goal_handle,
532542
CancelCallback cancel_callback = nullptr)
533543
{
534-
std::lock_guard<std::mutex> lock(goal_handles_mutex_);
544+
std::lock_guard<std::recursive_mutex> lock(goal_handles_mutex_);
535545
if (goal_handles_.count(goal_handle->get_goal_id()) == 0) {
536546
throw exceptions::UnknownGoalHandleError();
537547
}
@@ -562,6 +572,63 @@ class Client : public ClientBase
562572
return async_cancel(cancel_request, cancel_callback);
563573
}
564574

575+
/// Stops the callbacks for the goal in a thread safe way
576+
/**
577+
* This will NOT cancel the goal, it will only stop the callbacks.
578+
*
579+
* After the call to this function, it is guaranteed that there
580+
* will be no more callbacks from the goal. This is not guaranteed
581+
* if multiple threads are involved, and the goal_handle is just
582+
* dropped.
583+
*
584+
* \param[in] goal_handle The goal were the callbacks shall be stopped
585+
*/
586+
void stop_callbacks(typename GoalHandle::SharedPtr goal_handle)
587+
{
588+
goal_handle->set_feedback_callback(typename GoalHandle::FeedbackCallback());
589+
goal_handle->set_result_callback(typename GoalHandle::ResultCallback());
590+
591+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
592+
const GoalUUID & goal_id = goal_handle->get_goal_id();
593+
auto it = goal_handles_.find(goal_id);
594+
if (goal_handles_.end() == it) {
595+
// someone else already deleted the entry
596+
// e.g. the result callback
597+
RCLCPP_DEBUG(
598+
this->get_logger(),
599+
"Given goal is unknown. Ignoring...");
600+
return;
601+
}
602+
goal_handles_.erase(it);
603+
}
604+
605+
/// Stops the callbacks for the goal in a thread safe way
606+
/**
607+
* For futher information see stop_callbacks(typename GoalHandle::SharedPtr goal_handle)
608+
*/
609+
void stop_callbacks(const GoalUUID & goal_id)
610+
{
611+
typename GoalHandle::SharedPtr goal_handle;
612+
{
613+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
614+
auto it = goal_handles_.find(goal_id);
615+
if (goal_handles_.end() == it) {
616+
// someone else already deleted the entry
617+
// e.g. the result callback
618+
RCLCPP_DEBUG(
619+
this->get_logger(),
620+
"Given goal is unknown. Ignoring...");
621+
return;
622+
}
623+
624+
goal_handle = it->lock();
625+
}
626+
627+
if (goal_handle) {
628+
stop_callbacks(goal_handle);
629+
}
630+
}
631+
565632
/// Asynchronously request all goals at or before a specified time be canceled.
566633
/**
567634
* \param[in] stamp The timestamp for the cancel goal request.
@@ -590,7 +657,7 @@ class Client : public ClientBase
590657
virtual
591658
~Client()
592659
{
593-
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
660+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
594661
auto it = goal_handles_.begin();
595662
while (it != goal_handles_.end()) {
596663
typename GoalHandle::SharedPtr goal_handle = it->second.lock();
@@ -637,7 +704,7 @@ class Client : public ClientBase
637704
void
638705
handle_feedback_message(std::shared_ptr<void> message) override
639706
{
640-
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
707+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
641708
using FeedbackMessage = typename ActionT::Impl::FeedbackMessage;
642709
typename FeedbackMessage::SharedPtr feedback_message =
643710
std::static_pointer_cast<FeedbackMessage>(message);
@@ -674,7 +741,7 @@ class Client : public ClientBase
674741
void
675742
handle_status_message(std::shared_ptr<void> message) override
676743
{
677-
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
744+
std::lock_guard<std::recursive_mutex> guard(goal_handles_mutex_);
678745
using GoalStatusMessage = typename ActionT::Impl::GoalStatusMessage;
679746
auto status_message = std::static_pointer_cast<GoalStatusMessage>(message);
680747
for (const GoalStatus & status : status_message->status_list) {
@@ -723,7 +790,7 @@ class Client : public ClientBase
723790
wrapped_result.goal_id = goal_handle->get_goal_id();
724791
wrapped_result.code = static_cast<ResultCode>(result_response->status);
725792
goal_handle->set_result(wrapped_result);
726-
std::lock_guard<std::mutex> lock(goal_handles_mutex_);
793+
std::lock_guard<std::recursive_mutex> lock(goal_handles_mutex_);
727794
goal_handles_.erase(goal_handle->get_goal_id());
728795
});
729796
} catch (rclcpp::exceptions::RCLError & ex) {
@@ -755,7 +822,7 @@ class Client : public ClientBase
755822
}
756823

757824
std::map<GoalUUID, typename GoalHandle::WeakPtr> goal_handles_;
758-
std::mutex goal_handles_mutex_;
825+
std::recursive_mutex goal_handles_mutex_;
759826
};
760827
} // namespace rclcpp_action
761828

rclcpp_action/include/rclcpp_action/client_goal_handle_impl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ ClientGoalHandle<ActionT>::set_result(const WrappedResult & wrapped_result)
7575
result_promise_.set_value(wrapped_result);
7676
if (result_callback_) {
7777
result_callback_(wrapped_result);
78+
result_callback_ = ResultCallback();
7879
}
7980
}
8081

0 commit comments

Comments
 (0)