Skip to content

Commit de9db00

Browse files
committed
Fixed a weird threading bug with the wait mechanism in ServiceClient when the Service was not ready.
Added pendingRequests property.
1 parent 7231474 commit de9db00

File tree

2 files changed

+57
-43
lines changed

2 files changed

+57
-43
lines changed

include/qml_ros2_plugin/service_client.hpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ class ServiceClient : public QObjectRos2
2727
//! Connection timeout in ms to wait for the service to become available when sending a request.
2828
Q_PROPERTY( int connectionTimeout READ connectionTimeout WRITE setConnectionTimeout NOTIFY
2929
connectionTimeoutChanged )
30+
//! How many service requests are currently pending and waiting for the response.
31+
Q_PROPERTY( int pendingRequests READ pendingRequests NOTIFY pendingRequestsChanged )
32+
33+
using clock = std::chrono::steady_clock;
34+
3035
public:
3136
/*!
3237
* @param name The service topic.
@@ -48,6 +53,8 @@ class ServiceClient : public QObjectRos2
4853

4954
void setConnectionTimeout( int timeout );
5055

56+
int pendingRequests() const;
57+
5158
/*!
5259
* Calls a service asynchronously returning immediately.
5360
* Once the service call finishes, the optional callback is called with the result if provided.
@@ -64,6 +71,8 @@ class ServiceClient : public QObjectRos2
6471

6572
void connectionTimeoutChanged();
6673

74+
void pendingRequestsChanged();
75+
6776
protected:
6877
void onRos2Initialized() override;
6978

@@ -82,9 +91,15 @@ private slots:
8291
QString service_type_;
8392
ros_babel_fish::BabelFishServiceClient::SharedPtr client_;
8493
QTimer connect_timer_;
85-
std::vector<std::thread> waiting_threads_;
86-
std::atomic<bool> stop_{ false };
87-
int connection_timeout_ = 10'000; // Default connection timeout in ms
94+
struct WaitingServiceCallData {
95+
QVariantMap request;
96+
QJSValue callback;
97+
clock::time_point start;
98+
};
99+
// Queue of service calls waiting for the service to become available
100+
std::vector<WaitingServiceCallData> waiting_service_calls_;
101+
int pending_requests_ = 0;
102+
int connection_timeout_ = 10'000; // Default connection timeout for waiting service calls in ms
88103
};
89104
} // namespace qml_ros2_plugin
90105

src/service_client.cpp

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,7 @@ ServiceClient::ServiceClient( QString name, QString type, const QoSWrapper &qos
2121
babel_fish_ = BabelFishDispenser::getBabelFish();
2222
}
2323

24-
ServiceClient::~ServiceClient()
25-
{
26-
stop_ = true;
27-
for ( auto &thread : waiting_threads_ ) {
28-
if ( thread.joinable() )
29-
thread.join();
30-
}
31-
waiting_threads_.clear();
32-
}
24+
ServiceClient::~ServiceClient() = default;
3325

3426
void ServiceClient::onRos2Initialized()
3527
{
@@ -49,23 +41,43 @@ void ServiceClient::onRos2Initialized()
4941
connect_timer_.start();
5042
}
5143

52-
void ServiceClient::onRos2Shutdown()
53-
{
54-
stop_ = true;
55-
for ( auto &thread : waiting_threads_ ) {
56-
if ( thread.joinable() )
57-
thread.join();
58-
}
59-
client_.reset();
60-
}
44+
void ServiceClient::onRos2Shutdown() { client_.reset(); }
6145

6246
void ServiceClient::checkServiceReady()
6347
{
64-
if ( !isServiceReady() )
48+
if ( !isServiceReady() ) {
49+
int timeouted = 0;
50+
for ( size_t i = 0; i < waiting_service_calls_.size(); ++i, ++timeouted ) {
51+
if ( clock::now() - waiting_service_calls_[i].start <
52+
std::chrono::milliseconds( connection_timeout_ ) )
53+
break;
54+
QML_ROS2_PLUGIN_DEBUG(
55+
"Request for service '%s' timeouted while waiting for it to become ready.",
56+
name_.toStdString().c_str() );
57+
invokeCallback( waiting_service_calls_[i].callback, QVariant( false ) );
58+
pending_requests_--;
59+
}
60+
if ( timeouted > 0 ) {
61+
waiting_service_calls_.erase( waiting_service_calls_.begin(),
62+
waiting_service_calls_.begin() + timeouted );
63+
emit pendingRequestsChanged();
64+
}
6565
return;
66+
}
6667
connect_timer_.stop();
6768
disconnect( &connect_timer_, &QTimer::timeout, this, &ServiceClient::checkServiceReady );
6869
emit serviceReadyChanged();
70+
71+
if ( waiting_service_calls_.empty() )
72+
return;
73+
74+
blockSignals( true );
75+
for ( const auto &call : waiting_service_calls_ ) {
76+
pending_requests_--;
77+
sendRequestAsync( call.request, call.callback );
78+
}
79+
blockSignals( false );
80+
emit pendingRequestsChanged();
6981
}
7082

7183
bool ServiceClient::isServiceReady() const
@@ -87,32 +99,15 @@ void ServiceClient::setConnectionTimeout( int timeout )
8799
emit connectionTimeoutChanged();
88100
}
89101

102+
int ServiceClient::pendingRequests() const { return pending_requests_; }
103+
90104
void ServiceClient::sendRequestAsync( const QVariantMap &req, const QJSValue &callback )
91105
{
92-
using clock = std::chrono::steady_clock;
106+
pending_requests_++;
93107
if ( client_ == nullptr || !client_->service_is_ready() ) {
94108
QML_ROS2_PLUGIN_DEBUG( "Service '%s' not ready, waiting up to %d ms.",
95109
name_.toStdString().c_str(), connection_timeout_ );
96-
waiting_threads_.emplace_back( [this, req, callback] {
97-
auto now = clock::now();
98-
while ( !isServiceReady() ) {
99-
if ( stop_ || clock::now() - now > std::chrono::milliseconds( connection_timeout_ ) ) {
100-
QML_ROS2_PLUGIN_DEBUG( "Service '%s' timeouted or client was destroyed while waiting for "
101-
"it to become ready.",
102-
name_.toStdString().c_str() );
103-
QMetaObject::invokeMethod( this, "invokeCallback", Qt::AutoConnection,
104-
Q_ARG( QJSValue, callback ),
105-
Q_ARG( QVariant, QVariant( false ) ) );
106-
return;
107-
}
108-
std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) );
109-
}
110-
QML_ROS2_PLUGIN_DEBUG(
111-
"Service '%s' is ready after waiting %ld ms. Sending goal.", name_.toStdString().c_str(),
112-
std::chrono::duration_cast<std::chrono::milliseconds>( clock::now() - now ).count() );
113-
QMetaObject::invokeMethod( this, "sendRequestAsync", Qt::AutoConnection,
114-
Q_ARG( QVariantMap, req ), Q_ARG( QJSValue, callback ) );
115-
} );
110+
waiting_service_calls_.push_back( { req, callback, clock::now() } );
116111
return;
117112
}
118113
QML_ROS2_PLUGIN_DEBUG( "Service '%s' is ready. Sending request.", name_.toStdString().c_str() );
@@ -123,10 +118,13 @@ void ServiceClient::sendRequestAsync( const QVariantMap &req, const QJSValue &ca
123118
client_->async_send_request( message, [callback,
124119
this]( BabelFishServiceClient::SharedFuture response ) {
125120
QML_ROS2_PLUGIN_DEBUG( "Received response from service %s.", name_.toStdString().c_str() );
121+
pending_requests_--;
122+
emit pendingRequestsChanged();
126123
QMetaObject::invokeMethod( this, "invokeCallback", Qt::AutoConnection,
127124
Q_ARG( QJSValue, callback ),
128125
Q_ARG( QVariant, msgToMap( response.get() ) ) );
129126
} );
127+
emit pendingRequestsChanged();
130128
return;
131129
} catch ( BabelFishException &ex ) {
132130
QML_ROS2_PLUGIN_ERROR( "Failed to call service: %s", ex.what() );
@@ -135,6 +133,7 @@ void ServiceClient::sendRequestAsync( const QVariantMap &req, const QJSValue &ca
135133
} catch ( ... ) {
136134
QML_ROS2_PLUGIN_ERROR( "Failed to call service: Unknown error." );
137135
}
136+
pending_requests_--;
138137
QMetaObject::invokeMethod( this, "invokeCallback", Qt::AutoConnection,
139138
Q_ARG( QJSValue, callback ), Q_ARG( QVariant, QVariant( false ) ) );
140139
}

0 commit comments

Comments
 (0)