Motivation
Since the Lyrical release, the client library working group has been considering various approaches to reduce the amount of bloat in rclcpp and rclpy, remove or re-implement half-baked features, make behavior more consistent and performant across the different client library implementations, and foster more code reuse. I want to propose a concretely scoped step in the direction of “Less is More” with what seems like a fairly obvious candidate for de-duplication.
Currently, between rclcpp and rclpy, there are 7 different executor implementations:
rclcpp::executors::SingleThreadedExecutorrclcpp::executors::MultiThreadedExecutorrclcpp::executors::EventsCBGExecutor(newly introduced for Lyrical Luth)rclcpp::experimental::executors::EventsExecutor(already deprecated and slated for removal in Makoa Mata-Mata)rclpy.executors.SingleThreadedExecutorrclpy.executors.MultiThreadedExecutorrclpy.experimental.EventsExecutor
This isn’t even counting the new asyncio features introduced in rclpy, or rclrs’s own executor which has no timer support and none of the parallelism refinements introduced in the C++ version like multiple threads and callback groups. Each EventsExecutor shares the same fundamental architecture and includes its own timer_manager implementation. rclpy’s events executor is a pybind layer over its own roughly 1,500 lines of C++ that shares zero code with rclcpp.
Having this many executors in the codebase has the potential to cause confusion for users of the client libraries who may not have the context or bandwidth to pay attention to which ones are actively being maintained, as well as for new contributors as to which executor has which feature or implementation detail, or what’s worth opening a pull request against to fix vs what’s been deprecated.
Additionally, there has been extensive discussion among the maintainers about deprecating the traditional polling based executors and promoting the new EventsCBGExecutor to the default in ROS, (the one that’s instantiated when you call rclcpp::spin()). I wanted to use this post as an opportunity to expand on that idea in a way not directly coupled to rclcpp. At the time of writing there is at least one change regarding unbounded growth of the events queue we’d like to add to the new executor before it becomes the default, but even once that’s implemented, promoting to the default for just rclcpp feels incomplete and like we would be leaving the other client libraries behind.
Proposed Changes
- Decouple wait-sets from the
Executorbase class entirely. - Close out what remaining gaps exist in the
EventsCBGExecutorto promote it to the default executor. (this is also an area where we would really appreciate feedback from the community!) - Bring the
waitableAPI for python up to parity with C++, by addingset_on_ready_callback,clear_on_ready_callback,take_data_by_entity_id, etc. These were added to therclcppwaitable to support theEventsExecutorwhen it was being developed (alongside the rmw listener callback APIs implemented by FastDDS/CycloneDDS/Zenoh/Connext) but no such changes made it intorclpy. As a result, the python events executor can’t directly query a waitable for callbacks and has to do a bunch of complicated handling of a wait-set object instead to achieve the same result. - De-duplicate all of the core client library executor implementations into a new C++ package,
rcl_executors. Corerclremains a pure C package so as not to introduce C++ into embedded contexts, likerclc/micro-ros. - Move the
EventsCBGExecutorintorcl_executors. - Client libraries can use
rcl_executors::EventsCBGExecutor:rclpybecomes responsible for onlypybinding theEventsCBGExecutorinstead of having its own implementation.rclcpp::executors::EventsCBGExecutorsimply aliasesrcl_executors::EventsCBGExecutor.- the potential still exists for more language-idiomatic or alternate implementations, like
asyncio,rclc’s executor, etc, to be implemented if desired.
rclcpp::spin(),rclpy.spin(), etc instantiates anrcl_executors::EventsCBGExecutorandspins it- Deprecate (in m-turtle) and remove (by n-turtle) the polling / wait-set based implementations (
SingleThreadedExecutor,MultiThreadedExecutor) fromrclcppandrclpy.
Risks / Open Questions
- Existing references in user client library code to the old executors. As part of this proposal, to ease migration we could keep those symbols aliased for compatibility, i.e.
class SingleThreadedExecutor : public EventsCBGExecutor (with 1 thread)etc, but this carries potential issues for users implicitly dependent on the behavior of whichever variant of re-implemented executor logic they’re using, specifically the ordering of ready entities. - Support for custom, user-defined
Waitables may break by switching to a fully events-driven system under the hood - For other client library implementations besides
rclcpp/rclpy, what is the most ergonomic way to expose this proposed C++ executor implementation as a pure C API?
