rclcpp/rclcpp_action/include/rclcpp_action/client.hpp
Jacob Perron c7b62bff71
[rclcpp_action] Action client holds weak pointers to goal handles (#1122)
* [rclcpp_action] Action client holds weak pointers to goal handles

Fixes #861

It is against the design of ROS actions to rely on the status topic for the core implementation,
instead it should just be used for introspection.

Rather than relying on the status topic to remove references to goal handles, the action client
instead holds weak pointers to the goal handles. This way as long as a user holds a reference to
the goal handle they can use it to interact with the action client.

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

* Move cleanup logic to the end of the function

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

* Add TODO

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

* Log debug messages when dropping a weak references to goal handles

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

* Improve documentation

Signed-off-by: Jacob Perron <jacob@openrobotics.org>
2020-05-22 11:41:34 -07:00

680 lines
23 KiB
C++

// Copyright 2018 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef RCLCPP_ACTION__CLIENT_HPP_
#define RCLCPP_ACTION__CLIENT_HPP_
#include <rclcpp/macros.hpp>
#include <rclcpp/node_interfaces/node_base_interface.hpp>
#include <rclcpp/node_interfaces/node_logging_interface.hpp>
#include <rclcpp/node_interfaces/node_graph_interface.hpp>
#include <rclcpp/logger.hpp>
#include <rclcpp/time.hpp>
#include <rclcpp/waitable.hpp>
#include <rosidl_runtime_c/action_type_support_struct.h>
#include <rosidl_typesupport_cpp/action_type_support.hpp>
#include <algorithm>
#include <chrono>
#include <functional>
#include <future>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include "rclcpp_action/client_goal_handle.hpp"
#include "rclcpp_action/types.hpp"
#include "rclcpp_action/visibility_control.hpp"
namespace rclcpp_action
{
// Forward declaration
class ClientBaseImpl;
/// Base Action Client implementation
/// \internal
/**
* This class should not be used directly by users wanting to create an aciton client.
* Instead users should use `rclcpp_action::Client<>`.
*
* Internally, this class is responsible for interfacing with the `rcl_action` API.
*/
class ClientBase : public rclcpp::Waitable
{
public:
RCLCPP_ACTION_PUBLIC
virtual ~ClientBase();
/// Return true if there is an action server that is ready to take goal requests.
RCLCPP_ACTION_PUBLIC
bool
action_server_is_ready() const;
/// Wait for action_server_is_ready() to become true, or until the given timeout is reached.
template<typename RepT = int64_t, typename RatioT = std::milli>
bool
wait_for_action_server(
std::chrono::duration<RepT, RatioT> timeout = std::chrono::duration<RepT, RatioT>(-1))
{
return wait_for_action_server_nanoseconds(
std::chrono::duration_cast<std::chrono::nanoseconds>(timeout)
);
}
// -------------
// Waitables API
/// \internal
RCLCPP_ACTION_PUBLIC
size_t
get_number_of_ready_subscriptions() override;
/// \internal
RCLCPP_ACTION_PUBLIC
size_t
get_number_of_ready_timers() override;
/// \internal
RCLCPP_ACTION_PUBLIC
size_t
get_number_of_ready_clients() override;
/// \internal
RCLCPP_ACTION_PUBLIC
size_t
get_number_of_ready_services() override;
/// \internal
RCLCPP_ACTION_PUBLIC
size_t
get_number_of_ready_guard_conditions() override;
/// \internal
RCLCPP_ACTION_PUBLIC
bool
add_to_wait_set(rcl_wait_set_t * wait_set) override;
/// \internal
RCLCPP_ACTION_PUBLIC
bool
is_ready(rcl_wait_set_t * wait_set) override;
/// \internal
RCLCPP_ACTION_PUBLIC
void
execute() override;
// End Waitables API
// -----------------
protected:
RCLCPP_ACTION_PUBLIC
ClientBase(
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base,
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph,
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging,
const std::string & action_name,
const rosidl_action_type_support_t * type_support,
const rcl_action_client_options_t & options);
/// Wait for action_server_is_ready() to become true, or until the given timeout is reached.
RCLCPP_ACTION_PUBLIC
bool
wait_for_action_server_nanoseconds(std::chrono::nanoseconds timeout);
// -----------------------------------------------------
// API for communication between ClientBase and Client<>
using ResponseCallback = std::function<void (std::shared_ptr<void> response)>;
/// \internal
RCLCPP_ACTION_PUBLIC
rclcpp::Logger get_logger();
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
GoalUUID
generate_goal_id();
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
void
send_goal_request(
std::shared_ptr<void> request,
ResponseCallback callback);
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
void
send_result_request(
std::shared_ptr<void> request,
ResponseCallback callback);
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
void
send_cancel_request(
std::shared_ptr<void> request,
ResponseCallback callback);
/// \internal
virtual
std::shared_ptr<void>
create_goal_response() const = 0;
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
void
handle_goal_response(
const rmw_request_id_t & response_header,
std::shared_ptr<void> goal_response);
/// \internal
virtual
std::shared_ptr<void>
create_result_response() const = 0;
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
void
handle_result_response(
const rmw_request_id_t & response_header,
std::shared_ptr<void> result_response);
/// \internal
virtual
std::shared_ptr<void>
create_cancel_response() const = 0;
/// \internal
RCLCPP_ACTION_PUBLIC
virtual
void
handle_cancel_response(
const rmw_request_id_t & response_header,
std::shared_ptr<void> cancel_response);
/// \internal
virtual
std::shared_ptr<void>
create_feedback_message() const = 0;
/// \internal
virtual
void
handle_feedback_message(std::shared_ptr<void> message) = 0;
/// \internal
virtual
std::shared_ptr<void>
create_status_message() const = 0;
/// \internal
virtual
void
handle_status_message(std::shared_ptr<void> message) = 0;
// End API for communication between ClientBase and Client<>
// ---------------------------------------------------------
private:
std::unique_ptr<ClientBaseImpl> pimpl_;
};
/// Action Client
/**
* This class creates an action client.
*
* To create an instance of an action client use `rclcpp_action::create_client()`.
*
* Internally, this class is responsible for:
* - coverting between the C++ action type and generic types for `rclcpp_action::ClientBase`, and
* - calling user callbacks.
*/
template<typename ActionT>
class Client : public ClientBase
{
public:
RCLCPP_SMART_PTR_DEFINITIONS_NOT_COPYABLE(Client<ActionT>)
using Goal = typename ActionT::Goal;
using Feedback = typename ActionT::Feedback;
using GoalHandle = ClientGoalHandle<ActionT>;
using WrappedResult = typename GoalHandle::WrappedResult;
using GoalResponseCallback =
std::function<void (std::shared_future<typename GoalHandle::SharedPtr>)>;
using FeedbackCallback = typename GoalHandle::FeedbackCallback;
using ResultCallback = typename GoalHandle::ResultCallback;
using CancelRequest = typename ActionT::Impl::CancelGoalService::Request;
using CancelResponse = typename ActionT::Impl::CancelGoalService::Response;
using CancelCallback = std::function<void (typename CancelResponse::SharedPtr)>;
/// Options for sending a goal.
/**
* This struct is used to pass parameters to the function `async_send_goal`.
*/
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 future to 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`.
* If an error occurs while waiting for the goal response an exception will be thrown
* when calling `future::get()`.
* Possible exceptions include `rclcpp::RCLError` and `rclcpp::RCLBadAlloc`.
*/
GoalResponseCallback 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;
};
/// Construct an action client.
/**
* This constructs an action client, but it will not work until it has been added to a node.
* Use `rclcpp_action::create_client()` to both construct and add to a node.
*
* \param[in] node_base A pointer to the base interface of a node.
* \param[in] node_graph A pointer to an interface that allows getting graph information about
* a node.
* \param[in] node_logging A pointer to an interface that allows getting a node's logger.
* \param[in] action_name The action name.
* \param[in] client_options Options to pass to the underlying `rcl_action::rcl_action_client_t`.
*/
Client(
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base,
rclcpp::node_interfaces::NodeGraphInterface::SharedPtr node_graph,
rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging,
const std::string & action_name,
const rcl_action_client_options_t client_options = rcl_action_client_get_default_options()
)
: ClientBase(
node_base, node_graph, node_logging, action_name,
rosidl_typesupport_cpp::get_action_type_support_handle<ActionT>(),
client_options)
{
}
/// Send an action goal and asynchronously get the result.
/**
* If the goal is accepted by an action server, the returned future is set to a `ClientGoalHandle`.
* If the goal is rejected by an action server, then the future is set to a `nullptr`.
*
* The returned goal handle is used to monitor the status of the goal and get the final result.
* It is valid as long as you hold a reference to the shared pointer or until the
* rclcpp_action::Client is destroyed at which point the goal status will become UNKNOWN.
*
* \param[in] goal The goal request.
* \param[in] options Options for sending the goal request. Contains references to callbacks for
* the goal response (accepted/rejected), feedback, and the final result.
* \return A future that completes when the goal has been accepted or rejected.
* If the goal is rejected, then the result will be a `nullptr`.
*/
std::shared_future<typename GoalHandle::SharedPtr>
async_send_goal(const Goal & goal, const SendGoalOptions & options = SendGoalOptions())
{
// Put promise in the heap to move it around.
auto promise = std::make_shared<std::promise<typename GoalHandle::SharedPtr>>();
std::shared_future<typename GoalHandle::SharedPtr> future(promise->get_future());
using GoalRequest = typename ActionT::Impl::SendGoalService::Request;
auto goal_request = std::make_shared<GoalRequest>();
goal_request->goal_id.uuid = this->generate_goal_id();
goal_request->goal = goal;
this->send_goal_request(
std::static_pointer_cast<void>(goal_request),
[this, goal_request, options, promise, future](std::shared_ptr<void> response) mutable
{
using GoalResponse = typename ActionT::Impl::SendGoalService::Response;
auto goal_response = std::static_pointer_cast<GoalResponse>(response);
if (!goal_response->accepted) {
promise->set_value(nullptr);
if (options.goal_response_callback) {
options.goal_response_callback(future);
}
return;
}
GoalInfo goal_info;
goal_info.goal_id.uuid = goal_request->goal_id.uuid;
goal_info.stamp = goal_response->stamp;
// Do not use std::make_shared as friendship cannot be forwarded.
std::shared_ptr<GoalHandle> goal_handle(
new GoalHandle(goal_info, options.feedback_callback, options.result_callback));
{
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
goal_handles_[goal_handle->get_goal_id()] = goal_handle;
}
promise->set_value(goal_handle);
if (options.goal_response_callback) {
options.goal_response_callback(future);
}
if (options.result_callback) {
try {
this->make_result_aware(goal_handle);
} catch (...) {
promise->set_exception(std::current_exception());
return;
}
}
});
// TODO(jacobperron): Encapsulate into it's own function and
// consider exposing an option to disable this cleanup
// To prevent the list from growing out of control, forget about any goals
// with no more user references
{
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
auto goal_handle_it = goal_handles_.begin();
while (goal_handle_it != goal_handles_.end()) {
if (!goal_handle_it->second.lock()) {
RCLCPP_DEBUG(
this->get_logger(),
"Dropping weak reference to goal handle during send_goal()");
goal_handle_it = goal_handles_.erase(goal_handle_it);
} else {
++goal_handle_it;
}
}
}
return future;
}
/// Asynchronously get the result for an active goal.
/**
* \throws exceptions::UnknownGoalHandleError If the goal unknown or already reached a terminal
* state.
* \param[in] goal_handle The goal handle for which to get the result.
* \param[in] result_callback Optional callback that is called when the result is received.
* \return A future that is set to the goal result when the goal is finished.
*/
std::shared_future<WrappedResult>
async_get_result(
typename GoalHandle::SharedPtr goal_handle,
ResultCallback result_callback = nullptr)
{
std::lock_guard<std::mutex> lock(goal_handles_mutex_);
if (goal_handles_.count(goal_handle->get_goal_id()) == 0) {
throw exceptions::UnknownGoalHandleError();
}
if (result_callback) {
// This will override any previously registered callback
goal_handle->set_result_callback(result_callback);
}
this->make_result_aware(goal_handle);
return goal_handle->async_get_result();
}
/// Asynchronously request a goal be canceled.
/**
* \throws exceptions::UnknownGoalHandleError If the goal is unknown or already reached a
* terminal state.
* \param[in] goal_handle The goal handle requesting to be canceled.
* \param[in] cancel_callback Optional callback that is called when the response is received.
* The callback takes one parameter: a shared pointer to the CancelResponse message.
* \return A future to a CancelResponse message that is set when the request has been
* acknowledged by an action server.
* See
* <a href="https://github.com/ros2/rcl_interfaces/blob/master/action_msgs/srv/CancelGoal.srv">
* action_msgs/CancelGoal.srv</a>.
*/
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel_goal(
typename GoalHandle::SharedPtr goal_handle,
CancelCallback cancel_callback = nullptr)
{
std::lock_guard<std::mutex> lock(goal_handles_mutex_);
if (goal_handles_.count(goal_handle->get_goal_id()) == 0) {
throw exceptions::UnknownGoalHandleError();
}
auto cancel_request = std::make_shared<CancelRequest>();
// cancel_request->goal_info.goal_id = goal_handle->get_goal_id();
cancel_request->goal_info.goal_id.uuid = goal_handle->get_goal_id();
return async_cancel(cancel_request, cancel_callback);
}
/// Asynchronously request for all goals to be canceled.
/**
* \param[in] cancel_callback Optional callback that is called when the response is received.
* The callback takes one parameter: a shared pointer to the CancelResponse message.
* \return A future to a CancelResponse message that is set when the request has been
* acknowledged by an action server.
* See
* <a href="https://github.com/ros2/rcl_interfaces/blob/master/action_msgs/srv/CancelGoal.srv">
* action_msgs/CancelGoal.srv</a>.
*/
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel_all_goals(CancelCallback cancel_callback = nullptr)
{
auto cancel_request = std::make_shared<CancelRequest>();
// std::fill(cancel_request->goal_info.goal_id.uuid, 0u);
std::fill(
cancel_request->goal_info.goal_id.uuid.begin(),
cancel_request->goal_info.goal_id.uuid.end(), 0u);
return async_cancel(cancel_request, cancel_callback);
}
/// Asynchronously request all goals at or before a specified time be canceled.
/**
* \param[in] stamp The timestamp for the cancel goal request.
* \param[in] cancel_callback Optional callback that is called when the response is received.
* The callback takes one parameter: a shared pointer to the CancelResponse message.
* \return A future to a CancelResponse message that is set when the request has been
* acknowledged by an action server.
* See
* <a href="https://github.com/ros2/rcl_interfaces/blob/master/action_msgs/srv/CancelGoal.srv">
* action_msgs/CancelGoal.srv</a>.
*/
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel_goals_before(
const rclcpp::Time & stamp,
CancelCallback cancel_callback = nullptr)
{
auto cancel_request = std::make_shared<CancelRequest>();
// std::fill(cancel_request->goal_info.goal_id.uuid, 0u);
std::fill(
cancel_request->goal_info.goal_id.uuid.begin(),
cancel_request->goal_info.goal_id.uuid.end(), 0u);
cancel_request->goal_info.stamp = stamp;
return async_cancel(cancel_request, cancel_callback);
}
virtual
~Client()
{
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
auto it = goal_handles_.begin();
while (it != goal_handles_.end()) {
typename GoalHandle::SharedPtr goal_handle = it->second.lock();
if (goal_handle) {
goal_handle->invalidate();
}
it = goal_handles_.erase(it);
}
}
private:
/// \internal
std::shared_ptr<void>
create_goal_response() const override
{
using GoalResponse = typename ActionT::Impl::SendGoalService::Response;
return std::shared_ptr<void>(new GoalResponse());
}
/// \internal
std::shared_ptr<void>
create_result_response() const override
{
using GoalResultResponse = typename ActionT::Impl::GetResultService::Response;
return std::shared_ptr<void>(new GoalResultResponse());
}
/// \internal
std::shared_ptr<void>
create_cancel_response() const override
{
return std::shared_ptr<void>(new CancelResponse());
}
/// \internal
std::shared_ptr<void>
create_feedback_message() const override
{
using FeedbackMessage = typename ActionT::Impl::FeedbackMessage;
return std::shared_ptr<void>(new FeedbackMessage());
}
/// \internal
void
handle_feedback_message(std::shared_ptr<void> message) override
{
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
using FeedbackMessage = typename ActionT::Impl::FeedbackMessage;
typename FeedbackMessage::SharedPtr feedback_message =
std::static_pointer_cast<FeedbackMessage>(message);
const GoalUUID & goal_id = feedback_message->goal_id.uuid;
if (goal_handles_.count(goal_id) == 0) {
RCLCPP_DEBUG(
this->get_logger(),
"Received feedback for unknown goal. Ignoring...");
return;
}
typename GoalHandle::SharedPtr goal_handle = goal_handles_[goal_id].lock();
// Forget about the goal if there are no more user references
if (!goal_handle) {
RCLCPP_DEBUG(
this->get_logger(),
"Dropping weak reference to goal handle during feedback callback");
goal_handles_.erase(goal_id);
return;
}
auto feedback = std::make_shared<Feedback>();
*feedback = feedback_message->feedback;
goal_handle->call_feedback_callback(goal_handle, feedback);
}
/// \internal
std::shared_ptr<void>
create_status_message() const override
{
using GoalStatusMessage = typename ActionT::Impl::GoalStatusMessage;
return std::shared_ptr<void>(new GoalStatusMessage());
}
/// \internal
void
handle_status_message(std::shared_ptr<void> message) override
{
std::lock_guard<std::mutex> guard(goal_handles_mutex_);
using GoalStatusMessage = typename ActionT::Impl::GoalStatusMessage;
auto status_message = std::static_pointer_cast<GoalStatusMessage>(message);
for (const GoalStatus & status : status_message->status_list) {
const GoalUUID & goal_id = status.goal_info.goal_id.uuid;
if (goal_handles_.count(goal_id) == 0) {
RCLCPP_DEBUG(
this->get_logger(),
"Received status for unknown goal. Ignoring...");
continue;
}
typename GoalHandle::SharedPtr goal_handle = goal_handles_[goal_id].lock();
// Forget about the goal if there are no more user references
if (!goal_handle) {
RCLCPP_DEBUG(
this->get_logger(),
"Dropping weak reference to goal handle during status callback");
goal_handles_.erase(goal_id);
continue;
}
goal_handle->set_status(status.status);
}
}
/// \internal
void
make_result_aware(typename GoalHandle::SharedPtr goal_handle)
{
// Avoid making more than one request
if (goal_handle->set_result_awareness(true)) {
return;
}
using GoalResultRequest = typename ActionT::Impl::GetResultService::Request;
auto goal_result_request = std::make_shared<GoalResultRequest>();
goal_result_request->goal_id.uuid = goal_handle->get_goal_id();
this->send_result_request(
std::static_pointer_cast<void>(goal_result_request),
[goal_handle, this](std::shared_ptr<void> response) mutable
{
// Wrap the response in a struct with the fields a user cares about
WrappedResult wrapped_result;
using GoalResultResponse = typename ActionT::Impl::GetResultService::Response;
auto result_response = std::static_pointer_cast<GoalResultResponse>(response);
wrapped_result.result = std::make_shared<typename ActionT::Result>();
*wrapped_result.result = result_response->result;
wrapped_result.goal_id = goal_handle->get_goal_id();
wrapped_result.code = static_cast<ResultCode>(result_response->status);
goal_handle->set_result(wrapped_result);
std::lock_guard<std::mutex> lock(goal_handles_mutex_);
goal_handles_.erase(goal_handle->get_goal_id());
});
}
/// \internal
std::shared_future<typename CancelResponse::SharedPtr>
async_cancel(
typename CancelRequest::SharedPtr cancel_request,
CancelCallback cancel_callback = nullptr)
{
// Put promise in the heap to move it around.
auto promise = std::make_shared<std::promise<typename CancelResponse::SharedPtr>>();
std::shared_future<typename CancelResponse::SharedPtr> future(promise->get_future());
this->send_cancel_request(
std::static_pointer_cast<void>(cancel_request),
[cancel_callback, promise](std::shared_ptr<void> response) mutable
{
auto cancel_response = std::static_pointer_cast<CancelResponse>(response);
promise->set_value(cancel_response);
if (cancel_callback) {
cancel_callback(cancel_response);
}
});
return future;
}
std::map<GoalUUID, typename GoalHandle::WeakPtr> goal_handles_;
std::mutex goal_handles_mutex_;
};
} // namespace rclcpp_action
#endif // RCLCPP_ACTION__CLIENT_HPP_