4141#include < set>
4242#include < string>
4343#include < sstream>
44+ #include < thread>
4445#include < vector>
4546
4647#include < boost/algorithm/string/join.hpp>
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
158161WebVideoServer::~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
172192void 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
193252void 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