ROS Resources: Documentation | Support | Discussion Forum | Index | Service Status | ros @ Robotics Stack Exchange
Ask Your Question
1

Why is the Future 'done callback' order reversed?

asked 2020-12-09 02:46:51 -0500

SmallJoeMan gravatar image

updated 2020-12-09 02:50:51 -0500

I'd like to attach multiple callbacks to a Future provided by a ClientGoalHandle. However, when I do this I find that the callbacks are being called in reverse order. I'm a little stumped as to why this is happening. When I look through the code for rclpy.executor or rclpy.task I can only see for callback in callbacks.

Here's a snippet from my code:

@property
def logger(self):
    return self._node.get_logger()

def foo(self, *args, **kwargs):
    self.logger.info('foo')

def bar(self, *args, **kwargs):
    self.logger.info('bar')

def baz(self, *args, **kwargs):
    self.logger.info('baz')

def qux(self, *args, **kwargs):
    self.logger.info('qux')

def next(self) -> Optional[Future]:
    self._clear_cache()
    if self.goal_handle is not None:
        self.logger.info("Cancelling current guidance.")
        future = self.goal_handle.cancel_goal_async()
        future.add_done_callback(self.foo)
        future.add_done_callback(self.bar)

        self._goal_future.add_done_callback(self.baz)
        self._goal_future.add_done_callback(self.qux)

        return self._goal_future
    else:
        return None

Here is the console readout in which you can see bar is called before foo and qux is called before baz:

[simple_state_machine-2] [INFO] [1607502712.297026172] [state]: [GoalState] Cancelling current guidance.
[velocity_controller-3] [INFO] [1607502712.301784151] [vel_ctrl]: Received cancel request.
[simple_state_machine-2] [INFO] [1607502712.302841057] [state]: [GoalState] bar
[simple_state_machine-2] [INFO] [1607502712.303296070] [state]: [GoalState] foo
[velocity_controller-3] [INFO] [1607502712.397141220] [vel_ctrl]: Guidance cancelled.
[velocity_controller-3] [INFO] [1607502712.398128136] [vel_ctrl]: Goal status: 5.
[simple_state_machine-2] [INFO] [1607502712.399765926] [state]: [GoalState] qux
[simple_state_machine-2] [INFO] [1607502712.400109978] [state]: [GoalState] baz
[simple_state_machine-2] [WARN] [1607502712.400789882] [state]: [GoalState] Navigation ended with goal status code: 5 and guidance status code: 0.

I can 'fix' this by reversing the future callback order:

with future._lock:
    future._callbacks.reverse()

But it feels like a hack to mess around with private variables within the future class. Any ideas?

edit retag flag offensive close merge delete

1 Answer

Sort by ยป oldest newest most voted
1

answered 2020-12-18 13:25:02 -0500

sloretz gravatar image

There are two modes that future callbacks are called. Either they're called directly by the future, or they're scheduled with an executor. It's up to the executor to decide what order to call them in.

This example creates a future without an executor.

from functools import partial
from rclpy.task import Future

def callback(name, future):
    print(f"I am callback {name}")

f = Future()
f.add_done_callback(partial(callback, 'alice'))
f.add_done_callback(partial(callback, 'bob'))
f.add_done_callback(partial(callback, 'charlie'))
f.set_result("foobar")

It calls done callbacks in the order they're added

I am callback alice
I am callback bob
I am callback charlie

This future schedules callbacks with a SingleThreadedExecutor.

import rclpy
from rclpy.executors import SingleThreadedExecutor

rclpy.init()
s = SingleThreadedExecutor()
f = Future(executor=s)

f.add_done_callback(partial(callback, 'amy'))
f.add_done_callback(partial(callback, 'brice'))
f.add_done_callback(partial(callback, 'cole'))
f.set_result("foobar")

s.spin_once()
s.spin_once()
s.spin_once()
s.spin_once()

rclpy.shutdown()

It happens to call them in reverse.

I am callback cole
I am callback brice
I am callback amy

They're reversed because the executor checks which tasks are ready in reverse. Reversing the order means if an old task has yield'd or await'd because it's waiting for something to happen, then a new task (appended to the end of the task list) gets to run first and potentially unblock the old task.

That doesn't mean an executor is guaranteed to execute the tasks in reverse though. The order of callbacks in a Multithreaded executor depends on the order the operating system schedules threads.

import rclpy
from rclpy.executors import MultiThreadedExecutor

rclpy.init()
s = MultiThreadedExecutor()
f = Future(executor=s)

for i in range(100):
    f.add_done_callback(partial(callback, i))
    f.set_result("foobar")

for i in range(100):
    s.spin_once()

rclpy.shutdown()

It tends towards reverse order but is still pretty random

...
I am callback 95
I am callback 93
I am callback 92
I am callback 94
I am callback 89
I am callback 88
I am callback 90
...
edit flag offensive delete link more

Question Tools

2 followers

Stats

Asked: 2020-12-09 02:46:51 -0500

Seen: 801 times

Last updated: Dec 18 '20