Skip to content

Commit 6db2e1a

Browse files
committed
fix: Isolate DDS subscriptions to prevent FastDDS deadlock propagation
1 parent 5bbc5b5 commit 6db2e1a

5 files changed

Lines changed: 129 additions & 25 deletions

File tree

include/web_video_server/streamer.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,15 @@
3030

3131
#pragma once
3232

33+
#include <atomic>
3334
#include <chrono>
3435
#include <memory>
3536
#include <string>
3637
#include <vector>
3738

3839
#include "async_web_server_cpp/http_connection.hpp"
3940
#include "async_web_server_cpp/http_request.hpp"
41+
#include "rclcpp/callback_group.hpp"
4042
#include "rclcpp/logger.hpp"
4143
#include "rclcpp/node.hpp"
4244

@@ -83,6 +85,8 @@ class StreamerInterface
8385
* @brief Returns the client_id associated with this stream, or an empty string if none.
8486
*/
8587
virtual std::string get_client_id() = 0;
88+
89+
virtual rclcpp::CallbackGroup::SharedPtr get_callback_group() const { return nullptr; }
8690
};
8791

8892
/**
@@ -118,12 +122,15 @@ class StreamerBase : public StreamerInterface
118122
return client_id_;
119123
}
120124

125+
rclcpp::CallbackGroup::SharedPtr get_callback_group() const override { return callback_group_; }
126+
121127
protected:
122128
rclcpp::Node::SharedPtr lock_node() const;
123129

124130
async_web_server_cpp::HttpConnectionPtr connection_;
125131
async_web_server_cpp::HttpRequest request_;
126132
rclcpp::Node::WeakPtr node_;
133+
rclcpp::CallbackGroup::SharedPtr callback_group_;
127134
rclcpp::Logger logger_;
128135
std::atomic<bool> inactive_;
129136
std::string topic_;

include/web_video_server/web_video_server.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
#pragma once
3232

33+
#include <atomic>
3334
#include <chrono>
3435
#include <map>
3536
#include <memory>
@@ -121,12 +122,14 @@ class WebVideoServer : public rclcpp::Node
121122

122123
std::vector<std::shared_ptr<StreamerInterface>> streamers_;
123124
std::vector<std::shared_ptr<StreamerInterface>> pending_streamers_;
125+
std::vector<std::thread> streamer_threads_;
124126
pluginlib::ClassLoader<StreamerFactoryInterface> streamer_factory_loader_;
125127
std::map<std::string, std::shared_ptr<StreamerFactoryInterface>> streamer_factories_;
126128
pluginlib::ClassLoader<SnapshotStreamerFactoryInterface> snapshot_streamer_factory_loader_;
127129
std::map<std::string, std::shared_ptr<StreamerFactoryInterface>> snapshot_streamer_factories_;
128130
std::mutex streamers_mutex_;
129131
std::mutex pending_mutex_;
132+
std::atomic<bool> creation_in_progress_{false};
130133
};
131134

132135
} // namespace web_video_server

src/streamers/image_transport_streamer.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,14 @@ void ImageTransportStreamerBase::start()
147147
}
148148

149149
// Create subscriber
150+
callback_group_ = node->create_callback_group(
151+
rclcpp::CallbackGroupType::MutuallyExclusive, false);
152+
rclcpp::SubscriptionOptions sub_options;
153+
sub_options.callback_group = callback_group_;
150154
image_sub_ = image_transport::create_subscription(
151155
node.get(), topic_,
152156
std::bind(&ImageTransportStreamerBase::image_callback, this, std::placeholders::_1),
153-
default_transport_, qos_profile.value());
157+
default_transport_, qos_profile.value(), sub_options);
154158
}
155159

156160
#pragma GCC diagnostic pop

src/streamers/ros_compressed_streamer.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,15 @@ rclcpp::Subscription<CompressedImage>::SharedPtr create_compressed_image_subscri
114114
const std::string & qos_profile_name,
115115
const rclcpp::Node::SharedPtr & node,
116116
const rclcpp::Logger & logger,
117-
CallbackT && callback)
117+
CallbackT && callback,
118+
rclcpp::CallbackGroup::SharedPtr callback_group)
118119
{
119120
const std::string compressed_topic = topic + "/compressed";
120121
const auto qos = make_compressed_qos(compressed_topic, qos_profile_name, logger);
122+
rclcpp::SubscriptionOptions sub_options;
123+
sub_options.callback_group = callback_group;
121124
return node->create_subscription<CompressedImage>(
122-
compressed_topic, qos, std::forward<CallbackT>(callback));
125+
compressed_topic, qos, std::forward<CallbackT>(callback), sub_options);
123126
}
124127

125128
bool has_compressed_topic(rclcpp::Node & node, const std::string & topic)
@@ -185,9 +188,13 @@ void RosCompressedStreamer::start()
185188
return;
186189
}
187190

191+
callback_group_ = node->create_callback_group(
192+
rclcpp::CallbackGroupType::MutuallyExclusive, false);
193+
188194
image_sub_ = create_compressed_image_subscription(
189195
topic_, qos_profile_name_, node, logger_,
190-
std::bind(&RosCompressedStreamer::image_callback, this, std::placeholders::_1));
196+
std::bind(&RosCompressedStreamer::image_callback, this, std::placeholders::_1),
197+
callback_group_);
191198
}
192199

193200
void RosCompressedStreamer::restream_frame(std::chrono::duration<double> max_age)
@@ -300,9 +307,13 @@ void RosCompressedSnapshotStreamer::start()
300307
return;
301308
}
302309

310+
callback_group_ = node->create_callback_group(
311+
rclcpp::CallbackGroupType::MutuallyExclusive, false);
312+
303313
image_sub_ = create_compressed_image_subscription(
304314
topic_, qos_profile_name_, node, logger_,
305-
std::bind(&RosCompressedSnapshotStreamer::image_callback, this, std::placeholders::_1));
315+
std::bind(&RosCompressedSnapshotStreamer::image_callback, this, std::placeholders::_1),
316+
callback_group_);
306317
}
307318

308319
void RosCompressedSnapshotStreamer::restream_frame(std::chrono::duration<double>/* max_age */)

src/web_video_server.cpp

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include <set>
4242
#include <string>
4343
#include <sstream>
44+
#include <thread>
4445
#include <vector>
4546

4647
#include <boost/algorithm/string/join.hpp>
@@ -53,9 +54,11 @@
5354
#include "async_web_server_cpp/http_reply.hpp"
5455
#include "async_web_server_cpp/http_server.hpp"
5556
#include "pluginlib/exceptions.hpp"
57+
#include "rclcpp/executors/single_threaded_executor.hpp"
5658
#include "rclcpp/node.hpp"
5759
#include "rclcpp/node_options.hpp"
5860
#include "rclcpp/logging.hpp"
61+
#include "rclcpp/utilities.hpp"
5962

6063
#include "web_video_server/streamer.hpp"
6164

@@ -157,6 +160,23 @@ WebVideoServer::WebVideoServer(const rclcpp::NodeOptions & options)
157160

158161
WebVideoServer::~WebVideoServer()
159162
{
163+
// Mark all streamers inactive so their executor loops exit.
164+
{
165+
const std::scoped_lock lock(streamers_mutex_);
166+
for (auto & streamer : streamers_) {
167+
streamer->stop();
168+
}
169+
streamers_.clear();
170+
}
171+
172+
// Join all per-subscription threads before tearing down rclcpp context.
173+
for (auto & t : streamer_threads_) {
174+
if (t.joinable()) {
175+
t.join();
176+
}
177+
}
178+
streamer_threads_.clear();
179+
160180
server_->stop();
161181
}
162182

@@ -171,38 +191,97 @@ void WebVideoServer::restream_frames(std::chrono::duration<double> max_age)
171191

172192
void WebVideoServer::activate_pending_streamers()
173193
{
174-
std::vector<std::shared_ptr<StreamerInterface>> to_activate;
194+
// Activate only ONE pending streamer per timer tick. create_subscription()
195+
// in FastDDS serialises on a participant-level mutex, so spawning many
196+
// threads that all call it concurrently just makes them pile up. If one
197+
// gets stuck (FastDDS internal deadlock), all the others block behind it.
198+
// Processing one at a time limits the blast radius to a single thread.
199+
std::shared_ptr<StreamerInterface> streamer;
175200
{
176201
const std::scoped_lock lock(pending_mutex_);
177-
to_activate.swap(pending_streamers_);
178-
}
179-
180-
for (auto & streamer : to_activate) {
181-
streamer->start();
202+
if (pending_streamers_.empty()) {
203+
return;
204+
}
205+
streamer = pending_streamers_.front();
206+
pending_streamers_.erase(pending_streamers_.begin());
182207
}
183208

184-
if (!to_activate.empty()) {
209+
// Add to the active list immediately. The streamer won't produce output
210+
// until start() sets up its subscription, and cleanup_inactive_streams()
211+
// will collect it if start() fails (sets inactive_ = true).
212+
{
185213
const std::scoped_lock lock(streamers_mutex_);
186-
streamers_.insert(
187-
streamers_.end(),
188-
std::make_move_iterator(to_activate.begin()),
189-
std::make_move_iterator(to_activate.end()));
214+
streamers_.push_back(streamer);
190215
}
216+
217+
// Call start() on its own thread. create_subscription() can deadlock
218+
// inside FastDDS (lock-ordering between reader mutex and PDP/EDP mutexes),
219+
// so we must not block the timer callback on it.
220+
//
221+
// The thread also spins an isolated SingleThreadedExecutor for the
222+
// streamer's callback group. This keeps subscription callbacks completely
223+
// out of the main executor — if one DDS reader deadlocks, only that
224+
// stream's thread is affected; all other streams and the management
225+
// timers keep running.
226+
auto weak_node = weak_from_this();
227+
streamer_threads_.emplace_back([s = streamer, weak_node]() {
228+
s->start();
229+
if (s->is_inactive()) {
230+
return;
231+
}
232+
233+
auto node = weak_node.lock();
234+
if (!node) {
235+
return;
236+
}
237+
238+
auto cb_group = s->get_callback_group();
239+
if (!cb_group) {
240+
return;
241+
}
242+
243+
rclcpp::executors::SingleThreadedExecutor executor;
244+
executor.add_callback_group(cb_group, node->get_node_base_interface());
245+
246+
while (!s->is_inactive() && rclcpp::ok()) {
247+
executor.spin_once(std::chrono::milliseconds(100));
248+
}
249+
});
191250
}
192251

193252
void WebVideoServer::cleanup_inactive_streams()
194253
{
195-
const std::unique_lock lock(streamers_mutex_, std::try_to_lock);
196-
if (lock) {
197-
auto new_end = std::partition(
198-
streamers_.begin(), streamers_.end(),
199-
[](const std::shared_ptr<StreamerInterface> & streamer) {return !streamer->is_inactive();});
200-
if (verbose_) {
201-
for (auto itr = new_end; itr < streamers_.end(); ++itr) {
202-
RCLCPP_INFO(get_logger(), "Removed Stream: %s", (*itr)->get_topic().c_str());
254+
std::vector<std::shared_ptr<StreamerInterface>> to_destroy;
255+
{
256+
const std::unique_lock lock(streamers_mutex_, std::try_to_lock);
257+
if (lock) {
258+
auto new_end = std::partition(
259+
streamers_.begin(), streamers_.end(),
260+
[](const std::shared_ptr<StreamerInterface> & streamer) {
261+
return !streamer->is_inactive();
262+
});
263+
if (verbose_) {
264+
for (auto itr = new_end; itr < streamers_.end(); ++itr) {
265+
RCLCPP_INFO(get_logger(), "Removed Stream: %s", (*itr)->get_topic().c_str());
266+
}
203267
}
268+
// Move dead streamers out — don't destroy them while holding the lock
269+
// or on the executor thread.
270+
to_destroy.assign(
271+
std::make_move_iterator(new_end),
272+
std::make_move_iterator(streamers_.end()));
273+
streamers_.erase(new_end, streamers_.end());
204274
}
205-
streamers_.erase(new_end, streamers_.end());
275+
}
276+
277+
if (!to_destroy.empty()) {
278+
// Destroy on a detached thread. Subscription teardown calls
279+
// deleteUserEndpoint which waits on the DDS event thread — if that
280+
// thread is stuck (FastDDS internal deadlock), we'd block the entire
281+
// main executor. A throwaway thread can afford to wait.
282+
std::thread([captured = std::move(to_destroy)]() mutable {
283+
captured.clear();
284+
}).detach();
206285
}
207286
}
208287

0 commit comments

Comments
 (0)