Skip to content

Commit

Permalink
Fix stale Future done callbacks with spin_until_future_complete()
Browse files Browse the repository at this point in the history
This method can't be allowed to leave its Future done callback outstanding in case the method is
returning for a reason other than the Future being done.

Signed-off-by: Brad Martin <[email protected]>
  • Loading branch information
Brad Martin committed Jan 24, 2025
1 parent df3dbb7 commit 648b153
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 1 deletion.
14 changes: 14 additions & 0 deletions rclpy/rclpy/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,20 @@ def add_done_callback(self, callback: Callable[['Future[T]'], None]) -> None:
if invoke:
callback(self)

def remove_done_callback(self, callback: Callable[['Future[T]'], None]) -> bool:
"""
Removes a previously-added done callback.
Returns true if the given callback was found and removed. Always fails if the Future was
already complete.
"""
with self._lock:
try:
self._callbacks.remove(callback)
except ValueError:
return False
return True


class Task(Future[T]):
"""
Expand Down
9 changes: 8 additions & 1 deletion rclpy/src/rclpy/events_executor/events_executor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,15 @@ void EventsExecutor::spin(std::optional<double> timeout_sec, bool stop_after_use
void EventsExecutor::spin_until_future_complete(
py::handle future, std::optional<double> timeout_sec, bool stop_after_user_callback)
{
future.attr("add_done_callback")(py::cpp_function([this](py::handle) {io_context_.stop();}));
py::cpp_function cb([this](py::handle) {io_context_.stop();});
future.attr("add_done_callback")(cb);
spin(timeout_sec, stop_after_user_callback);
// In case the future didn't complete (we hit the timeout or dispatched a different user callback
// after being asked to only run one), we need to clean up our callback; otherwise, it could fire
// later when the executor isn't valid, or we haven't been asked to wait for this future; also,
// we could end up adding a bunch more of these same callbacks if this method gets invoked in a
// loop.
future.attr("remove_done_callback")(cb);
}

EventsExecutor * EventsExecutor::enter() {return this;}
Expand Down

0 comments on commit 648b153

Please sign in to comment.