test(mapoi_server): ノード破棄を context shutdown 後に集約し tf2 の join デッドロックを回避 - #462
Merged
Conversation
#461) `Nav2BridgeTestFixture` が毎テスト `MapoiNav2Bridge` を生成・破棄しており、1 run あたり 69 回ノードを作っていた。コンストラクタが作る単引数版 `TransformListener` は専用 executor スレッドを持ち、デストラクタで cancel() → join() する。ここに未修正の upstream race (ros2/geometry2#517) があり、cancel() が spin() 開始前に走ると cancel が失われて join() が 永久ブロックする。生成直後に破棄される短命な listener ほどこの窓に嵌まる。 - ノードを参照しない 45 テストは fixture からノード生成を撤去し、rclcpp::init の保証だけ残す - ノードが要る 13 テストは Nav2BridgeNodeFixture でスイート内 1 個を共有し、 共有に伴う順序依存は各テスト冒頭の明示リセットで潰す - 生成したノードは共有分も含め retain_node() で保持し、破棄を global test environment に集約。 そこで rclcpp::shutdown() を先に呼ぶことで spin() のループ条件を偽にし、cancel の 取りこぼしに関係なく join() が戻るようにする これでノード生成は 69 → 12 に減り、context shutdown 前に破棄されるノードは 0 になる。 本番コード (mapoi_server/src/) は変更していない。 2 CPU に制限しビジーループで飽和させた lyrical コンテナで、ctest と同じ 60 秒 timeout の 単発実行を試行した結果、ハングは main の 4/60 から 0/100 になった。 humble / jazzy / lyrical の 3 distro で colcon ゲート・テスト数 58・ --gtest_shuffle --gtest_repeat=3 がいずれも green。
mapoi_webui の e2e をローカル実行すると repo 直下に test-results/ が生成されるが gitignore に無く、git add -A で誤って commit に混入し得たため。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #461
背景
週次 schedule の
build-test (lyrical)がcolcon testでタイムアウト失敗した(該当 run)。
test_nav2_bridge_unitが ctest の 60 秒上限に到達し、結果ファイルも生成されなかった。ログの内訳は
[ RUN ]15 回 / コンストラクタ完了ログ 15 回 /[ OK ]14 回。15 個目のテスト本体は static 純関数への
EXPECT_TRUE1 行でnode_すら参照しないため、止まっていたのは本体ではなく
TearDown()のnode_.reset()だった。原因
MapoiNav2Bridgeのコンストラクタは単引数版 (=spin_thread=true) のTransformListenerを作る。これは内部ノードと専用 executor スレッドを持ち、デストラクタで
cancel()→join()する。ここに未修正の upstream race がある。
cancel()が専用スレッドのspin()開始前に走ると cancel シグナルが失われjoin()が永久ブロックするFixed deadlock in destructor in TransformListener ros2/geometry2#752 はいずれも未マージ。
humble と rolling でデストラクタ実装は同一で、両方とも未修正
生成直後に破棄される短命な listener ほどこの窓に嵌まる。
Nav2BridgeTestFixtureは毎テストMapoiNav2Bridgeを生成・破棄しており、1 run あたり 69 回の発火機会があった。各テストは 15–23 ms と短命で、窓に正確に嵌まっていた。
lyrical / humble の既定 RMW はどちらも
rmw_fastrtps_cppで、distro 差ではなく確率的な発火。CPU が逼迫する CI ランナーで顕在化する。
変更
本番コード (
mapoi_server/src/) は変更していない。テストのノード寿命管理だけを直した。Nav2BridgeTestFixtureからノード生成を撤去した。rclcpp::initの保証だけを残す。このフィクスチャを使う 45 テストはノードを参照していなかった。
node_を必要とする 13 テストは、新しいNav2BridgeNodeFixtureでスイート内 1 個を共有する。共有に伴う順序依存は、各テスト冒頭で前提を明示的に初期化して潰した。
retain_node()で保持し、破棄をglobal test environment の
TearDown()に集約する。そこではrclcpp::shutdown()を先に呼んでから破棄する。context が無効になればExecutor::spin()のループ条件が偽になるため、cancel シグナルの取りこぼしに関係なく~TransformListener()のjoin()が戻る。つまり残った 12 回の生成についても、破棄はすべて context shutdown 後に行われる。
race は確率的に減るのではなく構造的に起きなくなる。
実測
lyrical コンテナを 2 CPU に制限し、ビジーループ 2 本で飽和させた状態で、
ctest と同じ 60 秒 timeout・単発実行 (gtest_repeat なし) を試行した。
ハング位置は保全ログから特定した。main では
AngleDiffAbsZeroやBuildRoutePoiNamesEmptyなどフィクスチャがノードを作っていたテストで、手順 1–2 だけの版では
ConstructorTwistParamCreatesTwistSubなど本体でノードを作るテストで詰まっており、いずれも該当テストの所要が 59.9 秒に膨れていた。
短命破棄の件数とハング率が対応しており、機構と整合する。
検証
humble / jazzy / lyrical の 3 distro で以下を確認した。
colcon buildとcolcon test --ctest-args -LE launch_test+colcon test-result --verboseが green--gtest_shuffle --gtest_repeat=3が green (共有ノードに順序依存が無いこと)補足
ctestの timeout 延長や#ifdef UNIT_TESTによる本番コードへの分岐追加は採らなかった。前者は真のデッドロックに効かず、後者はテストした物と出荷する物が別になるため。
TransformListenerをspin_thread=falseにする案も見送った。本番の TF 更新スレッド構成が変わり、select_map_callbackが default group を最大 ~11s 塞ぐ設計 (#213) と干渉するため、CI flake 対策とは切り分けるべきと判断した。
ノードを retain した結果、共有ノードと
AutoResumeTimeout*のノードが既定cmd_velをbinary 終了まで掴み続ける。constructor test を追加する際は従来どおり専用
cmd_vel_topicが必要で、その理由をコメントに明記してある (#249)。
work in .my_brain