Skip to content

feat: support actions on the Tokio executor (Tokio Executor 3/3)#655

Draft
azerupi wants to merge 3 commits into
ros2-rust:mainfrom
azerupi:pr/3-actions
Draft

feat: support actions on the Tokio executor (Tokio Executor 3/3)#655
azerupi wants to merge 3 commits into
ros2-rust:mainfrom
azerupi:pr/3-actions

Conversation

@azerupi

@azerupi azerupi commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Note: This is based on #654 , I was hoping I could target that PR as base branch but that seems to only work if the branches are in the ros2_rust repo and not in my fork.


This is part of a set of PRs that attempt to add an event-driven Tokio executor to rclrs. The goal is to have something that:

  1. Works in a similar way than the events executor in rclcpp
  2. Interoperates nicely with the tokio ecosystem

I tried to split the work up into multiple PRs that build on top of each other to make the reviewing easier.

  1. feat: add push-callback registration for rcl primitives (Tokio Executor 1/3) #653
  2. feat: add an event-driven multi-threaded Tokio executor (Tokio Executor 2/3) #654
  3. feat: support actions on the Tokio executor (Tokio Executor 3/3) #655

support actions on the Tokio executor

This extends the event-driven Tokio executor to action servers and clients via the rcl_action push callbacks. It rounds out primitive coverage so the Tokio executor handles the same entity kinds as the basic executor.

An action is a composite primitive. A server bundles three services (goal, cancel, result). A client bundles two subscriptions (feedback, status) and three service clients (goal, cancel, result). Each internal source gets its own push callback, tagged with the readiness flag it satisfies, so the executor knows which part of the action became ready.

Because notifications for one entity coalesce, the executor cannot assume each mailbox message maps to a single source. It accumulates merged readiness per action entity: each notification ORs its flag in, and the worker takes the merged value when it runs the primitive. Single-path primitives (everything except actions) keep the cheaper path.

Goal expiration has no push callback. rcl drives it from an internal timer. The server polls it on a coarse interval. That is acceptable because expiration only bounds how promptly a completed goal is cleaned up, well after its result
timeout.

azerupi added 2 commits June 21, 2026 01:54
Add an opt-in way for primitives to report readiness via rcl's push
callbacks (rcl_*_set_on_new_*_callback) instead of being polled in a wait
set, as the foundation for an event-driven executor.

`RclPrimitive::register_on_ready` installs a callback that the middleware
invokes when the entity becomes ready and returns an `OnReadyHandle`
(RAII) that deregisters on drop. `OnReadyRegistration` wraps the unsafe
rcl setter: it boxes the callback context for a stable address and, on
drop, clears the callback before freeing the context (finalizing the rcl
entity first) so the middleware can never invoke a freed context during
teardown.

Implemented for subscriptions, services, and clients. No executor consumes
this yet, so the basic executor is unchanged.
Add a Tokio-based executor (opt-in via the `tokio-executor` feature,
enabled by default) that learns entity readiness from the rcl push
callbacks added in the previous commit instead of polling rcl_wait.

Each Worker drains its own mailbox on a dedicated Tokio task, so one
Worker's callbacks are serialized and ordered while independent Workers
run concurrently across Tokio's thread pool — multi-core concurrency with
no per-event task spawn. Subscriptions, services, and clients are driven
by push callbacks; timers by tokio::time. Worker tasks are gated by
spinning (callbacks only run while spinning, and spin() waits for
in-flight callbacks before returning); spin() honors
only_next_available_work (spin_once) and reports a timeout as a Timeout
error, matching the basic executor. Notifications coalesce per entity to
bound the mailbox, a panicking callback is contained rather than wedging
the worker, and push-callback registrations finalize the rcl entity before
freeing their context to avoid a teardown use-after-free.

Opt out with `default-features = false` to drop the Tokio multi-threaded
runtime and macros for a lighter build. Action support follows in a
separate commit.
@azerupi azerupi changed the title Pr/3 actions feat: support actions on the Tokio executor (Tokio Executor 3/3) Jun 21, 2026
@azerupi azerupi force-pushed the pr/3-actions branch 2 times, most recently from 0dd9e5b to dfccf93 Compare June 21, 2026 18:32
@azerupi azerupi force-pushed the pr/3-actions branch 4 times, most recently from e11770b to c77caf9 Compare June 21, 2026 19:49
Drive action servers and clients with rcl's action push callbacks
(rcl_action_*_set_*_callback) on the event-driven executor.

An action is a composite primitive: each internal source (the server's
goal/cancel/result services; the client's feedback/status subscriptions
and goal/cancel/result clients) registers its own push callback tagged
with the ReadyKind flag it satisfies. Because the notifications for one
action coalesce into a single mailbox wakeup, the executor accumulates a
merged ReadyKind per entity and runs the primitive with it. Goal
expiration has no push callback, so the server polls it on a coarse
interval.

Adds end-to-end tests for a goal round-trip (feedback + result) and for
cancellation on the Tokio executor.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant