Potential bug: if (is_clock_callback_group(callback_group) /* workaround */ || has_executor.load()) return
The following is provided by ChatGPT:
摘要(Summary)
在 ComponentManagerCallbackIsolated::add_node_to_executor() 中,針對僅含 /clock 訂閱的 callback group 以 is_clock_callback_group() 進行排除,導致該 group 沒有任何 executor 在 spin。當 use_sim_time=true 時,ROS Time 不會更新,所有 Timer 停止跳動,佇列也可能堆積。
上游 rclcpp 於 PR #1556 已證明「將 /clock callback 置於獨立執行緒」是官方修正方向。建議改為 方案 B:共用單一時鐘執行緒,兼顧資源與正確性。若無特殊需求,亦可直接移除此特例。
問題描述(Bug Description)
| 模組 | ComponentManagerCallbackIsolated |
||
| 程式碼位置 |
add_node_to_executor() 中的`if (is_clock_callback_group(...) |
has_executor.load())` | |
| 問題 |
/clock callback group 被跳過,未加入任何 executor |
||
| 環境 | ROS 2 Humble / rclcpp 28.2.1、Ubuntu 22.04 LTS、use_sim_time=true
|
重製步驟(How to Reproduce)
- 啟動 Gazebo 或播放 rosbag,確保
/clock正常發布。 - 將本程式碼中的 node 以 composable 方式載入。
- 建立一個依賴 ROS Time 的 Timer(例如 1 Hz 印 log)。
- 執行後可觀察到 Timer 不會觸發,且
rostopic echo /clock顯示時間仍在更新。
上述現象與 GitHub #1540、Discourse 帖子等回報完全一致。(GitHub, Open Robotics Discourse)
預期行為(Expected Behavior)
即使處於 callback 中,當 use_sim_time=true 時,node 的 get_clock()->now() 應隨 /clock 更新而遞增。(design.ros2.org)
實際行為(Actual Behavior)
/clock 訂閱 callback 未被任何 executor 觸發 → 時間凍結 → Timer 與 rclcpp::Rate 阻塞。
影響範圍(Impact)
- 功能停擺:所有模擬時間下的 Timer、sleep、rate 失效。(GitHub, Open Robotics Discourse)
- 資源浪費:/clock 佇列持續累積,最終可能丟訊息或耗盡記憶體。(GitHub)
-
除錯困難:僅於
use_sim_time啟用時才發生,無明顯錯誤訊息。
根因分析(Root Cause Analysis)
-
/clock group 未被執行
is_clock_callback_group()假設「只有一個 /clock 訂閱且無其他 entity」即代表可安全忽略;實際上忽略後便無執行緒更新 ROS Time。- 上游 rclcpp 早在 PR #1556 以「專用執行緒」修正同問題。(GitHub)
-
競態風險
has_executor.load()與後續add_callback_group()之間非原子操作,理論上仍可能被多執行緒同時通過而重複加入 executor,造成崩潰訊息Node has already been added to an executor。(Robotics Stack Exchange, GitHub, GitHub)
方案評估(Solutions & Trade-offs)
方案 A — 移除特例
-
說明:刪除
is_clock_callback_group(),讓每個 /clock group 仍由 caller 為其開 thread。 - 優點:邏輯最單純,無需額外維護。
- 缺點:每個 composable node 多一條 thread,對資源極度有限的平台不理想。
方案 B — 共用「時鐘執行緒」(推薦)
static auto clock_exec = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
static std::thread clock_thread([](){ clock_exec->spin(); });
if (is_clock_callback_group(callback_group)) {
clock_exec->add_callback_group(callback_group, node);
return;
}
| 優點 | 缺點 |
|---|---|
| 一條 thread 服務全部 /clock,CPU 與 stack 佔用最低。 | 需在程式結束時取消與 join,生命週期管理稍複雜。 |
| 可設定較高優先度或 CPU affinity,提高 time-stamp 準確度。 | 若未來 rclcpp 再次更動 TimeSource 實作,需檢查相容性。(GitHub) |
| 行為與官方 rclcpp #1556 「專屬執行緒」策略一致,易於升版調整。(GitHub) | 單點故障:若 thread 被阻塞,所有 /clock 更新延遲。 |
方案 C — 加回主要 executor
將 clock group 加入 ComponentManager 本身所屬 executor,不再獨立隔離。資源最省,但失去 thread 隔離初衷。
同步強化建議
改用 try_set_associated_with_executor() 或以 mutex 包裝 load()+add,避免競態。(ROS Answers)
與 CARET caret_topic_filter.bash 的關聯
CARET 官方文件建議在 trace 時忽略高頻且分析價值低的 /clock 與 /parameter_events,以縮減 trace 檔大小──這僅影響 trace 資料量,不會阻止 /clock callback 被 spin。(Tier4)
因此導入 方案 B 後,仍可維持 CARET 的預設過濾行為;若需量測 /clock latency,只要暫時修改 CARET_IGNORE_TOPICS 即可。
參考資料(References)
- rclcpp Issue #1540:
use_sim_time下時間凍結問題說明。(GitHub) - rclcpp PR #1556:Clock subscription spins in its own thread。(GitHub)
- Clock & Time 設計文件。(design.ros2.org)
- ROS Discourse:模擬時間不一致討論。(Open Robotics Discourse)
- rclcpp Issue #1542:建議將 /clock 放入專用 thread。(GitHub)
- rclcpp Issue #1454:Executor race condition 與 clock thread。(GitHub)
- Robotics SE:
Node has already been added to an executor錯誤。(Robotics Stack Exchange) - GitHub Issue #206:同上競態案例。(GitHub)
- GitHub Issue #2589:rclcpp 28.x regression 與 double-spin 問題。(GitHub)
- CARET官方文件:Trace filter 建議忽略
/clock。(Tier4) - StackExchange 探討 executor 使用限制。(Robotics Stack Exchange)
下一步(Next Actions)
- 實作方案 B 並於簡單模擬測試驗證 Timer 正常跳動。
- 加入 CI 測試:
ros2 bag play –clock→ 檢查 Timer callback 次數。 - 若確認穩定,再送 PR 至 upstream 或專案 fork。
若需更多細節或測試數據,歡迎回覆討論。