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)

  1. 啟動 Gazebo 或播放 rosbag,確保 /clock 正常發布。
  2. 將本程式碼中的 node 以 composable 方式載入。
  3. 建立一個依賴 ROS Time 的 Timer(例如 1 Hz 印 log)。
  4. 執行後可觀察到 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)

  1. /clock group 未被執行 is_clock_callback_group() 假設「只有一個 /clock 訂閱且無其他 entity」即代表可安全忽略;實際上忽略後便無執行緒更新 ROS Time。

    • 上游 rclcpp 早在 PR #1556 以「專用執行緒」修正同問題。(GitHub)
  2. 競態風險 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)

  1. rclcpp Issue #1540:use_sim_time 下時間凍結問題說明。(GitHub)
  2. rclcpp PR #1556:Clock subscription spins in its own thread。(GitHub)
  3. Clock & Time 設計文件。(design.ros2.org)
  4. ROS Discourse:模擬時間不一致討論。(Open Robotics Discourse)
  5. rclcpp Issue #1542:建議將 /clock 放入專用 thread。(GitHub)
  6. rclcpp Issue #1454:Executor race condition 與 clock thread。(GitHub)
  7. Robotics SE:Node has already been added to an executor 錯誤。(Robotics Stack Exchange)
  8. GitHub Issue #206:同上競態案例。(GitHub)
  9. GitHub Issue #2589:rclcpp 28.x regression 與 double-spin 問題。(GitHub)
  10. CARET官方文件:Trace filter 建議忽略 /clock。(Tier4)
  11. StackExchange 探討 executor 使用限制。(Robotics Stack Exchange)

下一步(Next Actions)

  1. 實作方案 B 並於簡單模擬測試驗證 Timer 正常跳動。
  2. 加入 CI 測試:ros2 bag play –clock → 檢查 Timer callback 次數。
  3. 若確認穩定,再送 PR 至 upstream 或專案 fork。

若需更多細節或測試數據,歡迎回覆討論。