rclpyの場合

では、rclpyではどうでしょうか。ソースコードは、このレポジトリに置きました。

 

rclcppと同様に、チュートリアルに示されているやり方からスタートしてみましょう。

client_within_action_1.py

server_execute_callbackの中で、チュートリアルと同じようにaction clientの各コールバックを設定してsend_goal_asyncを実行し、返されるfutureオブジェクトにadd_done_callbackで完了時のコールバックを設定します。しかし、これだとserver_execute_callbackの中でactionクライアントの結果を使うことができていないので、望みの動作が実現できません。

client_within_action_2.py

futureが返ってくるということは、その終了を待てるのではないか?ということで、server_execute_callbackの中で、チュートリアルにあるようにspin_future_until_complete()を呼んでみたくなりますよね?追加してみます。

1回目は動作します。しかし、2回目以降はサーバーの反応が無くなります。

rclcppで見たように、ROS 2はコールバックの中でspin()系関数を呼ぶことは想定されていないし、正しく動作しないのです。問題なのは、rclpyの場合、特に例外やエラーが発生することなく、なんとなく動作してしまうことです。

そもそもダメなのですが、もう少し追加してみましょう。

client_within_action_3.py

rclcppと同じようにexecutorとcallback groupの設定が必要なのではないか? ということで、callback groupをReentrantにし、executorをMultThreadedExecutorにしてみます。

なんと、ちゃんと動いているように見えます。コードもロジックとして分かりやすい。しかし、これが落とし穴です。何度も言いますが、コールバックの中でspin()系関数を呼ぶのはダメです。正しく動いているように見えてもダメなのです。次の例で分かります。

client_within_action_4.py

action serverの中で呼び出すクライアントを2つに増やしてみましょう。

一見、動いているように見えますが、/proxyの呼び出しを何度も繰り返してみると、以下のエラーが不定のタイミングで起こり、ノードが落ちてしまいます。

[ERROR] [1716008497.673642837] [client_within_action_node]: Error raised in execute callback: Failed to get number of ready entities for action client: wait set index for feedback subscription is out of bounds, at ./src/rcl_action/action_client.c:619
Traceback (most recent call last):
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/action/server.py”, line 332, in _execute_goal
    execute_result = await await_or_execute(execute_callback, goal_handle)
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py”, line 107, in await_or_execute
    return callback(*args)
  File “/home/tajima/Codes/ros2_ws/install/client_within_action_py/lib/python3.10/site-packages/client_within_action_py/client_within_action_node_4.py”, line 59, in server_execute_callback
    self.executor.spin_until_future_complete(future_2)
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py”, line 303, in spin_until_future_complete
    self.spin_once_until_future_complete(future, timeout_sec)
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py”, line 801, in spin_once_until_future_complete
    self._spin_once_impl(timeout_sec, future.done)
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py”, line 775, in _spin_once_impl
    handler, entity, node = self.wait_for_ready_callbacks(
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py”, line 711, in wait_for_ready_callbacks
    return next(self._cb_iter)
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/executors.py”, line 630, in _wait_for_ready_callbacks
    if wt in waitables and wt.is_ready(wait_set):
  File “/opt/ros/humble/local/lib/python3.10/dist-packages/rclpy/action/client.py”, line 231, in is_ready
    ready_entities = self._client_handle.is_ready(wait_set)
rclpy._rclpy_pybind11.RCLError: Failed to get number of ready entities for action client: wait set index for feedback subscription is out of bounds, at ./src/rcl_action/action_client.c:619

やはり、コールバックの中でspin_until_future_complete()のようなspin系の関数を呼び出してはいけないのです。しかも、rclpyでは明示的なエラーや例外が出ず、条件によっては正しく動いているように見えてしまいます。これが非常にハマりやすく、実際気づかずにこのようにノードを作成していた時がありました…

client_within_action_5.py

futureオブジェクトはawaitableなので、ひょっとしてawaitで待てるのではないか?やってみると、やはり一見正しく動作しているようにみえます。しかしこちらも、本来の使い方ではないのでメモリリークしていて、繰り返し/proxyを呼びつづけるとおかしなことが起こります。非常に厄介です。

client_within_action_6.py

ではどうするのが良いのでしょうか?rclpyのfutureには、rclcppのwait_for()のような関数が用意されていません。なので、future.done()を使って処理の終了をチェックしつつ、busy waitを行うのがベストプラクティスと考えています。その場合MultiThreadedExecutorを使い、callback groupもReentrantに設定することで、他のコールバックの処理を別スレッドで行えるようにしなければならないのは、rclcppと同様です。

これで、ちゃんと動くようになりました!

おわりに

rclcppでは、コールバック内でspin_until_future_complete()などのspin()系関数を呼ぶと例外が発生するので、そのまま漫然と使ってしまうことは無いでしょう。調べれば、コールバック内でクライアントを呼ぶことができるコードにたどり着けるはずです(正直、チュートリアルに正しい例をはっきりと書いておいてほしいですが…)。

しかしrclpyでは、やってはいけないこと(コールバック内でspin()系関数を呼ぶこと)をやっても、エラーも警告も出ません。下手をするとしばらくは動き続けるので、気づきにくくて非常に厄介です。インターネット上でのフォーラムを見ると、これに関連する質問がけっこう定期的に出てきています。

最後にもう一度:コールバックの中でspin()系関数を呼んではいけません。

どうぞ皆さんも罠に気を付けつつ、ハッピーなROS 2ライフをお送りください。